fix: a shared ticker no longer hides one of its two real tokens (closes #276) #277
Reference in New Issue
Block a user
Delete Branch "fix/issue-276-duplicate-symbol-addresses"
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?
Closes #276.
Seven tokens in our own bundled list were judged spoofs of their own symbol at their own address, and so were hidden from the balance list, the transaction history and the send token selector. A holder of any of them could not spend it through the UI.
The guard test, written first, failing with exactly the seven
A walk over
TOKENSassertingisSpoofedSymbol(t.symbol, t.address) === false, added before any data or code change:The suite could not have caught this before, because it walked
KNOWN_SYMBOLS— which is derived fromTOKENS— so it could only ever assert that the table agreed with itself.Provenance: there are not two tables
KNOWN_SYMBOLSis not a second source. It is built at module load fromTOKENS, first-wins in market-cap order:So both addresses of each conflicting pair are entries in the bundled list, from the same fetch: CoinGecko, 2026-02-27, decimals verified on-chain, per the header of
src/shared/tokenList.js. 512 tokens, 505 distinct uppercased symbols, seven symbols appearing twice. The table kept the lower index; the higher one was filtered.That changes the answer to the canonicality question the issue poses. Neither address is stale relative to the other — they were generated together — and there is no evidence in the tree that would elevate one over the other, so nothing was picked and nothing was dropped. Per symbol, in list order, with the name and index each entry carries in
TOKENS:TON0x582d872A…Toncoin, idx 150x2be5e8c1…Tokamak Network, idx 336FRAX0x853d955a…Legacy Frax Dollar, idx 840x3432B6A6…Frax (prev. FXS), idx 198REUSD0x5086bf35…Re Protocol reUSD, idx 1370x57aB1E00…Resupply USD, idx 278EURE0x39b8B638…Monerium EUR emoney, idx 3420x3231Cb76…Monerium EUR emoney [OLD], idx 355[OLD]; it is still a real contract with real holders.MSUSD0x4ba01f22…Main Street USD, idx 3660xab5eB14c…Metronome Synth USD, idx 372MUSD0xacA92E43…MetaMask USD, idx 3800xdD468A1D…Mezo USD, idx 437JPYC0x431D5dfF…JPY Coin, idx 4220x2370f9d5…JPY Coin v1, idx 441v1is the older one and still real.Three of the seven pairs are the same issuer's old and new contract, and it is tempting to call the newer one canonical and the older one obsolete. That is the wrong move for this filter. The filter does not decide what is worth holding; it decides what is a fake. A legacy Frax, an
[OLD]Monerium euro and a JPYC v1 are all contracts the user may genuinely hold today, and hiding a holding is the failure mode this issue is about. All fourteen addresses stay.No address was invented, and no address outside the bundled list was added — a test asserts that every address the table vouches for is a bundled token that reports that symbol, so an unverified entry cannot creep in later.
The duplicate-ticker decision
The table must hold a set of addresses per symbol, and now does. A one-address-per-symbol table cannot represent data in which a ticker belongs to two real contracts, and the previous behaviour — silently filtering whichever contract came second in a market-cap-ordered list — is the worst available answer, because the choice is arbitrary and its consequence is a hidden holding.
isSpoofedSymbol()asks set membership where it asked equality:This does not weaken the check. Every member of a set is an address the wallet ships as a real token; a contract outside the set is still a spoof, and a spoof of
FRAXfrom an unrelated contract is filtered exactly as before. Two tests pin that direction: one asserts a third contract bearing each shared ticker is still judged a spoof, and the existingUSDC/ETHspoof tests are unchanged. The alternative fix — dropping the seven ambiguous symbols from the table — was rejected precisely because it would weaken the check: it would stop filtering spoofs ofFRAX,TON,MUSDand the rest entirely.The native-asset entry is untouched:
ETHstill maps tonull, still meaning no contract may bear it. The build loop skips null entries, so an ERC-20 reporting the native symbol cannot add itself to a set and thereby claim the symbol.Third failure mode: none found
Checked, and asserted in the suite so it stays checked:
KNOWN_SYMBOLSvalue names an address absent fromTOKENS(the table is derived, and the new test would catch a hand-written entry).toUpperCase()key — all 512 are ASCII printable with no whitespace, so the lookup key and the normalized symbol always agree, and no bundled symbol is unreachable in the table.ETH.What was preserved
Verified unchanged, since this touches the same rule as #235 and #260:
ETH, and the test adding a second null-mapped symbol still asserts it inherits both halves.src/shared/transactions.js,src/popup/views/send.js,src/shared/balances.js— still read the rule fromsrc/shared/symbolSpoof.jswith no local copies.Verification
make check: green. 27 suites, 664 tests.script/test-verify-build18/18 cases passed; prettier clean.make test-e2e: green, 27/27 in the pinned Playwright container.FAIL —
needs-rebaseSole defect. The change itself is correct and verified; it no longer merges.
TODO.md— conflicts with currentnext.nextadvanced tod5595c0("test: drive the EIP-1193 dApp approval round trips in the browser", #183) after this branch was cut ate4c3708. Both commits insert a bullet at the top of# Completed Steps.Reproduction:
Gitea now reports
mergeable: falsefor this PR (it wastruewhen opened, befored5595c0landed).Acceptable: rebase onto current
origin/next, keeping this PR's bullet at the top of# Completed Stepsabove thed5595c0bullet and losing neither. Single commit,make checkandmake test-e2ere-run after the rebase. No other change required.Verified independently, all pass
Premise correction confirmed (
KNOWN_SYMBOLSis derived fromTOKENSat module load, first-wins; 512 tokens / 505 distinct uppercased symbols / 7 duplicated). Failing-first reproduced ate4c3708: exactly the seven named. All 512 bundled tokens pass at their own address on the head, by my own walk. Set totals 512 addresses across 506 entries (498 singletons, 7 pairs,ETH->null) — exactly the 14 intended, nothing else widened. Address normalisation is symmetric (toLowerCaseon both sides); checksummed, lowercase, uppercase-hex and0X-prefixed all match. NativeETHunfiltered at empty/null/undefinedcontract;legit === nullreturns before any.has(). FakeETHand third contracts bearing each of the seven shared tickers still filtered, as are cross-pair queries (FRAXat the Toncoin address,USDCat the WETH address). Fold vstoUpperCase()divergence: 0 across all 512; no symbol folds ontoETH; no unreachable table key. All seven canonicality rows match the names, addresses and indices inTOKENS; no invented or out-of-tree address. Mutations all caught: keying native on the symbol -> second-null-mapped-symbol test fails; fold widened to\p{Cc}-> visible-controls test fails;U+007Fdropped -> invisible-characters test fails; build loop reverted to first-wins -> 3 new tests fail; membership check replaced byfalse-> 8 tests fail. Three surfaces read the rule fromsrc/shared/symbolSpoof.js, no local copies.make checkgreen (27 suites / 664 tests,test-verify-build18/18, prettier clean);make test-e2e27/27 in the pinned container — both executed here, not cached. CI green onc8e1933(33 s). Commit hygiene, authorship, scope and terminology clean.Disclosures
normalizeAddress()lowercases without trimming. Identical one4c3708— pre-existing, not a regression, and it errs toward filtering. Recorded, not charged against this PR.src/shared/tokenList.js:6says// 511 tokens.; there are 512, and the comment this PR adds at line 3610 correctly says 512, so the file now contradicts itself. Pre-existing error, out of scope here.// MetaMask USDis added as an address annotation intests/symbolSpoof.test.js:387, mirroring thenamefield of the bundled entry. Judged data annotation rather than prose, consistent with existing occurrences in the tree; not charged.c8e193335dtoc66cec2f8bclawbot referenced this pull request2026-08-12 13:36:49 +02:00