fix: F-2026-18196 | [Dual Defense] Native SOL Misclassified as SPL Due to EVM-Only isNative Check - #307
fix: F-2026-18196 | [Dual Defense] Native SOL Misclassified as SPL Due to EVM-Only isNative Check#307Aman035 wants to merge 4 commits into
Conversation
|
@0xNilesh There are two pSOL rows for solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1, both pointing at address: 11111111111111111111111111111111 enabled: true GetTokenConfigByPRC20 matches on PRC20 + chain, returns the first hit, and does not Two things worth a look:
Separately, reverts take a different path: build_revert_outbound.go copies |
F-2026-18196 SVM builder native check missed Solana's native marker
Problem
0x0, or the EVM zero hex.11111111111111111111111111111111(SystemProgram, the base58 zero pubkey).CreateAssociatedTokenAccountIdempotentagainst SystemProgram, which is not a mint, so the transaction fails pre-broadcast.What actually reaches us, confirmed by core dev and verified on donut
11111111111111111111111111111111for native, then changed to the EVM zero after discussion. Both forms are therefore live.0x0000000000000000000000000000000000000000.create_outbound.gocopies the registry token address, and the registry has two pSOL rows sharing one PRC20, where the disabled EVM-zero row wins the lookup.11111111111111111111111111111111.build_revert_outbound.gocopiesinbound.AssetAddr, and the SVM parser sets that from the pubkey, so it is base58.EiXDnrAg9ea2Q6vEPV7E5TpTU1vh41jcuZqKjU5Dc4ZF, and were never affected.So withdrawals worked, which is why SDK tests stayed green. The revert and rescue path is the one that breaks.
What I did
isNativeAsset(addr string) boolhelper and used it at all three decision sites:GetOutboundSigningRequest,BuildOutboundTransaction,BuildRefRouteTransactions.isNativeas a parameter, so rescue and revert inherit the fix.buildAndSimulateRescuetest helper at the same function. It had its own copy of the check, so tests were not exercising the production path.Why it checks IsZero, not a string constant
base58ToHex("11111111111111111111111111111111")is 32 zero bytes, seeevent_parser_test.go.IsZero(), coveringSystemProgramID, the literal marker, and any other zero-pubkey spelling.PublicKeyFromBytespanics on a short slice.Tests
TestIsNativeAsset: core withdrawal form, core revert form, other zero spellings, real SPL mints (USDT.sol,USDC.sol,TokenProgramID) stay non-native, malformed input stays non-native.TestNativeMarkerFormsBuildIdenticalAccounts: builds the account list from both marker forms and asserts they are byte identical, and that no recipient ATA appears. That is the failure the old check produced.Worth flagging outside this PR
GetTokenConfigByPRC20has noenabledfilter and its comment assumes PRC20 addresses are unique, but two pSOL rows share one PRC20. Withdrawals currently work because the disabled row happens to sort first.11111111111111111111111111111111for withdrawals too. This PR is what keeps that from becoming an outage.