From a6bdd129a53ffcb3aae930136fc164127e6f8379 Mon Sep 17 00:00:00 2001 From: aman035 Date: Tue, 18 Aug 2026 16:21:57 +0530 Subject: [PATCH 1/4] fix: recognize Solana native marker in SVM builder isNative check (F-2026-18196) --- universalClient/chains/svm/tx_builder.go | 23 +++++++++++--- universalClient/chains/svm/tx_builder_test.go | 31 ++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/universalClient/chains/svm/tx_builder.go b/universalClient/chains/svm/tx_builder.go index 784ac1d3..ac91fc9e 100644 --- a/universalClient/chains/svm/tx_builder.go +++ b/universalClient/chains/svm/tx_builder.go @@ -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 { @@ -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 { @@ -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)) @@ -1282,6 +1281,22 @@ func removeHexPrefix(s string) string { return s } +// isNativeAsset reports whether addr denotes native SOL rather than an SPL mint. +// Native is the zero address in whichever encoding the registry supplies: empty, +// EVM zero hex, or the base58 zero pubkey — which is the Solana native marker +// SystemProgram (11111111111111111111111111111111). Treating that marker as a +// mint builds an SPL transfer that reverts, since SystemProgram is not a mint. +func isNativeAsset(addr string) bool { + switch addr { + case "", "0x0", "0x0000000000000000000000000000000000000000": + return true + } + if pubkey, err := solana.PublicKeyFromBase58(addr); err == nil { + return pubkey.IsZero() + } + return false +} + // ============================================================================= // PDA Derivation & On-Chain Data // ============================================================================= diff --git a/universalClient/chains/svm/tx_builder_test.go b/universalClient/chains/svm/tx_builder_test.go index 26842fc5..bc5ab416 100644 --- a/universalClient/chains/svm/tx_builder_test.go +++ b/universalClient/chains/svm/tx_builder_test.go @@ -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 { @@ -2718,3 +2718,32 @@ func TestSimulate_RefRoute_Execute(t *testing.T) { require.NoError(t, err) requireSimulationSuccess(t, storeSim) } + +// The SVM builder must recognise Solana's native marker, not only EVM zero +// addresses. Shipped native SOL / pSOL config stores SystemProgram; treating it +// as an SPL mint builds an ATA-create against a non-mint and reverts pre-broadcast. +func TestIsNativeAsset(t *testing.T) { + t.Run("solana native markers", func(t *testing.T) { + assert.True(t, isNativeAsset("11111111111111111111111111111111"), "shipped SystemProgram marker") + assert.True(t, isNativeAsset(solana.SystemProgramID.String())) + assert.True(t, isNativeAsset(solana.PublicKey{}.String()), "zero pubkey") + }) + + t.Run("evm-style markers still recognised", func(t *testing.T) { + assert.True(t, isNativeAsset("")) + assert.True(t, isNativeAsset("0x0")) + assert.True(t, isNativeAsset("0x0000000000000000000000000000000000000000")) + }) + + t.Run("real SPL mints are not native", func(t *testing.T) { + // USDC devnet mint. + assert.False(t, isNativeAsset("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")) + // Token program is a valid pubkey but not the zero address. + 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")) + }) +} From 2b55131eab06bf1040e189bc4c0d7a5e53be2ccb Mon Sep 17 00:00:00 2001 From: aman035 Date: Tue, 18 Aug 2026 16:30:56 +0530 Subject: [PATCH 2/4] fix: also treat hex-encoded zero pubkey as native SOL (F-2026-18196) --- universalClient/chains/svm/tx_builder.go | 5 +++++ universalClient/chains/svm/tx_builder_test.go | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/universalClient/chains/svm/tx_builder.go b/universalClient/chains/svm/tx_builder.go index ac91fc9e..87941f44 100644 --- a/universalClient/chains/svm/tx_builder.go +++ b/universalClient/chains/svm/tx_builder.go @@ -1294,6 +1294,11 @@ func isNativeAsset(addr string) bool { 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 } diff --git a/universalClient/chains/svm/tx_builder_test.go b/universalClient/chains/svm/tx_builder_test.go index bc5ab416..e5b2e910 100644 --- a/universalClient/chains/svm/tx_builder_test.go +++ b/universalClient/chains/svm/tx_builder_test.go @@ -2729,10 +2729,14 @@ func TestIsNativeAsset(t *testing.T) { assert.True(t, isNativeAsset(solana.PublicKey{}.String()), "zero pubkey") }) - t.Run("evm-style markers still recognised", func(t *testing.T) { + // The shipped registry carries both pSOL markers: the base58 SystemProgram + // and a legacy 20-byte EVM zero. A hex-encoded zero pubkey is covered too, + // since the builder accepts hex mints. + t.Run("hex markers still recognised", func(t *testing.T) { assert.True(t, isNativeAsset("")) assert.True(t, isNativeAsset("0x0")) - assert.True(t, isNativeAsset("0x0000000000000000000000000000000000000000")) + assert.True(t, isNativeAsset("0x0000000000000000000000000000000000000000"), "shipped legacy pSOL marker") + assert.True(t, isNativeAsset("0x"+strings.Repeat("0", 64)), "hex-encoded zero pubkey") }) t.Run("real SPL mints are not native", func(t *testing.T) { From 68e6811b4cb7c6d54dd1804553afa7ad569b0033 Mon Sep 17 00:00:00 2001 From: aman035 Date: Tue, 18 Aug 2026 16:50:07 +0530 Subject: [PATCH 3/4] docs: record that core sends EVM zero hex for solana native, base58 on reverts (F-2026-18196) --- universalClient/chains/svm/tx_builder.go | 8 ++--- universalClient/chains/svm/tx_builder_test.go | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/universalClient/chains/svm/tx_builder.go b/universalClient/chains/svm/tx_builder.go index 87941f44..a297da82 100644 --- a/universalClient/chains/svm/tx_builder.go +++ b/universalClient/chains/svm/tx_builder.go @@ -1282,10 +1282,10 @@ func removeHexPrefix(s string) string { } // isNativeAsset reports whether addr denotes native SOL rather than an SPL mint. -// Native is the zero address in whichever encoding the registry supplies: empty, -// EVM zero hex, or the base58 zero pubkey — which is the Solana native marker -// SystemProgram (11111111111111111111111111111111). Treating that marker as a -// mint builds an SPL transfer that reverts, since SystemProgram is not a 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": diff --git a/universalClient/chains/svm/tx_builder_test.go b/universalClient/chains/svm/tx_builder_test.go index e5b2e910..a4ea8a8a 100644 --- a/universalClient/chains/svm/tx_builder_test.go +++ b/universalClient/chains/svm/tx_builder_test.go @@ -2719,30 +2719,35 @@ func TestSimulate_RefRoute_Execute(t *testing.T) { requireSimulationSuccess(t, storeSim) } -// The SVM builder must recognise Solana's native marker, not only EVM zero -// addresses. Shipped native SOL / pSOL config stores SystemProgram; treating it -// as an SPL mint builds an ATA-create against a non-mint and reverts pre-broadcast. +// 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("solana native markers", func(t *testing.T) { - assert.True(t, isNativeAsset("11111111111111111111111111111111"), "shipped SystemProgram marker") + 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()), "zero pubkey") + assert.True(t, isNativeAsset(solana.PublicKey{}.String())) }) - // The shipped registry carries both pSOL markers: the base58 SystemProgram - // and a legacy 20-byte EVM zero. A hex-encoded zero pubkey is covered too, - // since the builder accepts hex mints. - t.Run("hex markers still recognised", func(t *testing.T) { + t.Run("other zero spellings are native", func(t *testing.T) { assert.True(t, isNativeAsset("")) assert.True(t, isNativeAsset("0x0")) - assert.True(t, isNativeAsset("0x0000000000000000000000000000000000000000"), "shipped legacy pSOL marker") 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) { - // USDC devnet mint. - assert.False(t, isNativeAsset("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")) - // Token program is a valid pubkey but not the zero address. + assert.False(t, isNativeAsset("EiXDnrAg9ea2Q6vEPV7E5TpTU1vh41jcuZqKjU5Dc4ZF"), "USDT.sol") + assert.False(t, isNativeAsset("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"), "USDC.sol") assert.False(t, isNativeAsset(solana.TokenProgramID.String())) }) From e30829cad58be215d6b5daf8928b5eb13a04f5e3 Mon Sep 17 00:00:00 2001 From: aman035 Date: Tue, 18 Aug 2026 17:05:26 +0530 Subject: [PATCH 4/4] test: assert both native marker forms build identical accounts (F-2026-18196) --- universalClient/chains/svm/tx_builder_test.go | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/universalClient/chains/svm/tx_builder_test.go b/universalClient/chains/svm/tx_builder_test.go index a4ea8a8a..ff1dcce0 100644 --- a/universalClient/chains/svm/tx_builder_test.go +++ b/universalClient/chains/svm/tx_builder_test.go @@ -2756,3 +2756,45 @@ func TestIsNativeAsset(t *testing.T) { 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") + } +}