fix: seven bundled tokens are filtered as spoofs of their own duplicate symbol #276
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
KNOWN_SYMBOLSmaps seven symbols to a different contract address than the oneTOKENSships for that same symbol, soisSpoofedSymbol(token.symbol, token.address)returns true for a token in our own bundled list, at its own address.Affected:
FRAX(KNOWN_SYMBOLShas0x3432B6A6…,TOKENShas0x853d955a…),REUSD,TON,EURE,MSUSD,MUSD,JPYC.This is the over-filtering direction — the one that hides a user's real holding. A user holding any of these seven sees it filtered from the balance list, the transaction history and the send selector, which means they cannot spend it through the UI.
Pre-existing and identical on
next; found by the independent review of #270 and confirmed unchanged by that PR, which is why it was not blocked there. The cause is duplicate symbols in the source data: two real contracts legitimately share a ticker, and the two tables disagree about which one is canonical.The reason no test caught it is that the spoof-filter tests walk
KNOWN_SYMBOLSonly. A walk overTOKENSassertingisSpoofedSymbol(t.symbol, t.address) === falsewould have.Implementation requirements
TOKENSlist and asserting no bundled token is filtered at its own address. That is the regression guard and it should go in whether or not the data is reconciled the way you expect.Definition of done
TOKENS.KNOWN_SYMBOLS.TODO.mdupdated in the same commit.make checkpasses.Plan, after establishing the provenance the issue asks for.
There are not two tables.
KNOWN_SYMBOLSis built at module load fromTOKENSitself (src/shared/tokenList.js), first-wins by market-cap order:So both addresses in each of the seven pairs come from the same CoinGecko fetch of 2026-02-27 with decimals verified on-chain. Neither is stale relative to the other, and there is no external source to prefer one over the other: 512 tokens, 505 distinct uppercased symbols, seven appearing twice.
KNOWN_SYMBOLSholds the earlier index and the later one is filtered.That settles the duplicate-ticker question rather than leaving it open: the seven are genuinely shared tickers between two real bundled contracts, so a one-address-per-symbol table cannot represent the data we ship. I will make
KNOWN_SYMBOLSmap a symbol to aSetof every bundled address that bears it (stillnullfor the native asset), and haveisSpoofedSymbol()ask set membership. Dropping the ambiguous symbols from the table instead would stop filtering genuine spoofs ofFRAX,TONand the rest, so that is not on the table.Third failure mode checked and clean: no
KNOWN_SYMBOLSvalue points at an address absent fromTOKENS, no bundled symbol normalizes differently from its uppercase key, and no bundled symbol folds ontoETH.Guard test is written and failing first with exactly the seven, before any data change. The native exemption keyed on absence of a contract address, the U+007F folding boundary pinned from both sides, and the single-module rule read by all three surfaces are all preserved; a new test asserts a spoof of a shared ticker from a third contract is still filtered.
Built in #277 — full reconciliation, the per-symbol determination table and the duplicate-ticker decision are in that PR body.
Short version:
KNOWN_SYMBOLSnow maps a symbol to the set of addresses that bear it. All fourteen addresses of the seven pairs stay; none was picked over another, because both sides of every pair come from the same source fetch and neither is stale.isSpoofedSymbol()asks set membership instead of equality, which does not loosen the rule.Verification: the guard test walking
TOKENSwas written first and failed with exactly the seven named in the issue, with the rest of the suite passing. After the fix,make checkgreen (27 suites, 664 tests;script/test-verify-build18/18; prettier clean) andmake test-e2egreen 27/27 in the pinned container. Third failure mode checked and clean: no table entry names an address outside the bundled list.