Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions universalClient/chains/svm/tx_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,8 @@ func (tb *TxBuilder) GetOutboundSigningRequest(
}

// Determine if this is native SOL or an SPL token transfer.
// Empty or zero address = native SOL. Otherwise it's the SPL token mint address.
assetAddr := data.AssetAddr
isNative := assetAddr == "" || assetAddr == "0x0" || assetAddr == "0x0000000000000000000000000000000000000000"
isNative := isNativeAsset(assetAddr)

txType, err := parseTxType(data.TxType)
if err != nil {
Expand Down Expand Up @@ -707,7 +706,7 @@ func (tb *TxBuilder) BuildOutboundTransaction(
}

assetAddr := data.AssetAddr
isNative := assetAddr == "" || assetAddr == "0x0" || assetAddr == "0x0000000000000000000000000000000000000000"
isNative := isNativeAsset(assetAddr)

txType, err := parseTxType(data.TxType)
if err != nil {
Expand Down Expand Up @@ -1052,7 +1051,7 @@ func (tb *TxBuilder) BuildRefRouteTransactions(
}

assetAddr := data.AssetAddr
isNative := assetAddr == "" || assetAddr == "0x0" || assetAddr == "0x0000000000000000000000000000000000000000"
isNative := isNativeAsset(assetAddr)

var txID [32]byte
txIDBytes, err := hex.DecodeString(removeHexPrefix(data.TxID))
Expand Down Expand Up @@ -1282,6 +1281,27 @@ func removeHexPrefix(s string) string {
return s
}

// isNativeAsset reports whether addr denotes native SOL rather than an SPL mint.
// Both encodings of the zero address reach us. Core sends the EVM zero hex on
// withdrawals (registry token address), while reverts carry the base58 zero
// pubkey, SystemProgram 11111111111111111111111111111111, copied from the
// inbound. SPL mints are always base58 and parse as an ordinary non-zero pubkey.
func isNativeAsset(addr string) bool {
switch addr {
case "", "0x0", "0x0000000000000000000000000000000000000000":
return true
}
if pubkey, err := solana.PublicKeyFromBase58(addr); err == nil {
return pubkey.IsZero()
}
// The builder also accepts hex mints, so cover a hex-encoded zero pubkey.
// Length must be checked first: PublicKeyFromBytes panics on a short slice.
if raw, err := hex.DecodeString(removeHexPrefix(addr)); err == nil && len(raw) == 32 {
return solana.PublicKeyFromBytes(raw).IsZero()
}
return false
}

// =============================================================================
// PDA Derivation & On-Chain Data
// =============================================================================
Expand Down
82 changes: 81 additions & 1 deletion universalClient/chains/svm/tx_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2391,7 +2391,7 @@ func buildAndSimulateRescue(t *testing.T, rpcClient *RPCClient, builder *TxBuild
require.NoError(t, err)
copy(sender[:], senderBytes)

isNative := assetAddr == ""
isNative := isNativeAsset(assetAddr)
var token [32]byte
var mintPubkey solana.PublicKey
if !isNative {
Expand Down Expand Up @@ -2718,3 +2718,83 @@ func TestSimulate_RefRoute_Execute(t *testing.T) {
require.NoError(t, err)
requireSimulationSuccess(t, storeSim)
}

// Both encodings of native SOL reach the builder, verified against donut:
// withdrawals carry the EVM zero hex from the registry token address, reverts
// carry the base58 SystemProgram marker copied from the inbound. Missing either
// builds an SPL transfer whose ATA-create reverts, since neither is a mint.
func TestIsNativeAsset(t *testing.T) {
t.Run("core withdrawal form is native", func(t *testing.T) {
// create_outbound.go copies the registry token address verbatim.
assert.True(t, isNativeAsset("0x0000000000000000000000000000000000000000"))
})

t.Run("core revert form is native", func(t *testing.T) {
// build_revert_outbound.go copies inbound.AssetAddr, which the SVM parser
// sets from the pubkey, so native SOL arrives base58 encoded.
assert.True(t, isNativeAsset("11111111111111111111111111111111"))
assert.True(t, isNativeAsset(solana.SystemProgramID.String()))
assert.True(t, isNativeAsset(solana.PublicKey{}.String()))
})

t.Run("other zero spellings are native", func(t *testing.T) {
assert.True(t, isNativeAsset(""))
assert.True(t, isNativeAsset("0x0"))
assert.True(t, isNativeAsset("0x"+strings.Repeat("0", 64)), "hex-encoded zero pubkey")
})

// SPL mints are base58 in both directions, so they parse normally and must
// keep taking the token path.
t.Run("real SPL mints are not native", func(t *testing.T) {
assert.False(t, isNativeAsset("EiXDnrAg9ea2Q6vEPV7E5TpTU1vh41jcuZqKjU5Dc4ZF"), "USDT.sol")
assert.False(t, isNativeAsset("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"), "USDC.sol")
assert.False(t, isNativeAsset(solana.TokenProgramID.String()))
})

t.Run("malformed addresses are not native", func(t *testing.T) {
assert.False(t, isNativeAsset("not-base58-0OlI"))
assert.False(t, isNativeAsset("0x1234"))
})
}

// Core sent the base58 marker before switching to the EVM zero, so both forms
// are live: withdrawals carry the zero hex and reverts still carry base58. Both
// must build the identical native account layout, with no recipient ATA.
func TestNativeMarkerFormsBuildIdenticalAccounts(t *testing.T) {
builder := newTestBuilder(t)

caller := solana.NewWallet().PublicKey()
config := solana.NewWallet().PublicKey()
vault := solana.NewWallet().PublicKey()
cea := solana.NewWallet().PublicKey()
tss := solana.NewWallet().PublicKey()
executed := solana.NewWallet().PublicKey()
recipient := solana.NewWallet().PublicKey()

build := func(t *testing.T, assetAddr string) []*solana.AccountMeta {
t.Helper()
isNative := isNativeAsset(assetAddr)
require.True(t, isNative, "asset %q must classify as native", assetAddr)
return builder.buildWithdrawAndExecuteAccounts(
caller, config, vault, cea, tss, executed,
solana.SystemProgramID,
isNative, 1,
recipient, solana.PublicKey{},
nil,
solana.PublicKey{}, solana.PublicKey{},
)
}

withdrawForm := build(t, "0x0000000000000000000000000000000000000000")
revertForm := build(t, "11111111111111111111111111111111")

assert.Equal(t, withdrawForm, revertForm,
"revert-form native SOL must build the same accounts as withdraw-form")

// The old bug took the SPL path and derived an ATA for a non-mint.
ata, _, err := solana.FindAssociatedTokenAddress(recipient, solana.SystemProgramID)
require.NoError(t, err)
for _, acc := range revertForm {
assert.NotEqual(t, ata, acc.PublicKey, "native layout must not include a recipient ATA")
}
}
Loading