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 TOKENS asserting isSpoofedSymbol(t.symbol, t.address) === false, added before any data or code change:
● the shipped token list › no bundled token is filtered at its own address
- Array []
+ Array [
+ "FRAX @ 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0",
+ "REUSD @ 0x57aB1E0003F623289CD798B1824Be09a793e4Bec",
+ "TON @ 0x2be5e8c109e2197D077D13A82dAead6a9b3433C5",
+ "EURE @ 0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f",
+ "MSUSD @ 0xab5eB14c09D416F0aC63661E57EDB7AEcDb9BEfA",
+ "MUSD @ 0xdD468A1DDc392dcdbEf6db6e34E89AA338F9F186",
+ "JPYC @ 0x2370f9d504c7a6E775bf6E14B3F12846b594cD53",
+ ]
The suite could not have caught this before, because it walked KNOWN_SYMBOLS — which is derived from TOKENS — so it could only ever assert that the table agreed with itself.
Provenance: there are not two tables
KNOWN_SYMBOLS is not a second source. It is built at module load from TOKENS, 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:
Symbol
Kept (was in the table)
Kept (was being filtered)
Determination
TON
0x582d872A… Toncoin, idx 15
0x2be5e8c1… Tokamak Network, idx 336
Two unrelated projects sharing a ticker. Both canonical for their own contract.
FRAX
0x853d955a… Legacy Frax Dollar, idx 84
0x3432B6A6… Frax (prev. FXS), idx 198
Same issuer, two live contracts. The list names one "Legacy", so it is the older one — but a legacy contract still holds real balances, and hiding them is the harm here.
REUSD
0x5086bf35… Re Protocol reUSD, idx 137
0x57aB1E00… Resupply USD, idx 278
Two unrelated issuers sharing a ticker. Both canonical.
EURE
0x39b8B638… Monerium EUR emoney, idx 342
0x3231Cb76… Monerium EUR emoney [OLD], idx 355
Same issuer, migration. The list marks one [OLD]; it is still a real contract with real holders.
MSUSD
0x4ba01f22… Main Street USD, idx 366
0xab5eB14c… Metronome Synth USD, idx 372
Two unrelated issuers sharing a ticker. Both canonical.
MUSD
0xacA92E43… MetaMask USD, idx 380
0xdD468A1D… Mezo USD, idx 437
Two unrelated issuers sharing a ticker. Both canonical.
JPYC
0x431D5dfF… JPY Coin, idx 422
0x2370f9d5… JPY Coin v1, idx 441
Same issuer, versioned. v1 is 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.
constKNOWN_SYMBOLS=newMap();// symbol -> Set of lowercased addresses, or null
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 FRAX from 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 existing USDC/ETH spoof 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 of FRAX, TON, MUSD and the rest entirely.
The native-asset entry is untouched: ETH still maps to null, 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:
No KNOWN_SYMBOLS value names an address absent from TOKENS (the table is derived, and the new test would catch a hand-written entry).
No bundled symbol normalizes differently under the spoof rule's fold than under the table's 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.
No bundled symbol folds onto ETH.
No bundled address appears twice, and no entry has a blank symbol or a malformed address.
What was preserved
Verified unchanged, since this touches the same rule as #235 and #260:
The native exemption still keys on absence of a contract address, not on the symbol being ETH, and the test adding a second null-mapped symbol still asserts it inherits both halves.
The symbol fold (NFKC, strip what paints nothing including U+007F, trim, uppercase) is untouched, and its boundary is still pinned from both sides: the visible-controls test and the invisible-characters test both still run and both still constrain it.
All three surfaces — src/shared/transactions.js, src/popup/views/send.js, src/shared/balances.js — still read the rule from src/shared/symbolSpoof.js with no local copies.
make test-e2e: green, 27/27 in the pinned Playwright container.
Failing-first evidence above was produced on this branch before the data-shape change, with the rest of the suite passing.
Closes [#276](https://git.eeqj.de/sneak/AutistMask/issues/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 `TOKENS` asserting `isSpoofedSymbol(t.symbol, t.address) === false`, added before any data or code change:
```
● the shipped token list › no bundled token is filtered at its own address
- Array []
+ Array [
+ "FRAX @ 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0",
+ "REUSD @ 0x57aB1E0003F623289CD798B1824Be09a793e4Bec",
+ "TON @ 0x2be5e8c109e2197D077D13A82dAead6a9b3433C5",
+ "EURE @ 0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f",
+ "MSUSD @ 0xab5eB14c09D416F0aC63661E57EDB7AEcDb9BEfA",
+ "MUSD @ 0xdD468A1DDc392dcdbEf6db6e34E89AA338F9F186",
+ "JPYC @ 0x2370f9d504c7a6E775bf6E14B3F12846b594cD53",
+ ]
```
The suite could not have caught this before, because it walked `KNOWN_SYMBOLS` — which is derived from `TOKENS` — so it could only ever assert that the table agreed with itself.
## Provenance: there are not two tables
`KNOWN_SYMBOLS` is not a second source. It is built at module load from `TOKENS`, first-wins in market-cap order:
```js
KNOWN_SYMBOLS.set("ETH", null);
for (const t of TOKENS) {
const upper = t.symbol.toUpperCase();
if (!KNOWN_SYMBOLS.has(upper)) KNOWN_SYMBOLS.set(upper, t.address.toLowerCase());
}
```
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`:
| Symbol | Kept (was in the table) | Kept (was being filtered) | Determination |
| --- | --- | --- | --- |
| `TON` | `0x582d872A…` Toncoin, idx 15 | `0x2be5e8c1…` Tokamak Network, idx 336 | Two unrelated projects sharing a ticker. Both canonical for their own contract. |
| `FRAX` | `0x853d955a…` Legacy Frax Dollar, idx 84 | `0x3432B6A6…` Frax (prev. FXS), idx 198 | Same issuer, two live contracts. The list names one "Legacy", so it is the older one — but a legacy contract still holds real balances, and hiding them is the harm here. |
| `REUSD` | `0x5086bf35…` Re Protocol reUSD, idx 137 | `0x57aB1E00…` Resupply USD, idx 278 | Two unrelated issuers sharing a ticker. Both canonical. |
| `EURE` | `0x39b8B638…` Monerium EUR emoney, idx 342 | `0x3231Cb76…` Monerium EUR emoney [OLD], idx 355 | Same issuer, migration. The list marks one `[OLD]`; it is still a real contract with real holders. |
| `MSUSD` | `0x4ba01f22…` Main Street USD, idx 366 | `0xab5eB14c…` Metronome Synth USD, idx 372 | Two unrelated issuers sharing a ticker. Both canonical. |
| `MUSD` | `0xacA92E43…` MetaMask USD, idx 380 | `0xdD468A1D…` Mezo USD, idx 437 | Two unrelated issuers sharing a ticker. Both canonical. |
| `JPYC` | `0x431D5dfF…` JPY Coin, idx 422 | `0x2370f9d5…` JPY Coin v1, idx 441 | Same issuer, versioned. `v1` is 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.
```js
const KNOWN_SYMBOLS = new Map(); // symbol -> Set of lowercased addresses, or null
```
`isSpoofedSymbol()` asks set membership where it asked equality:
```js
const legit = KNOWN_SYMBOLS.get(sym);
if (legit === null) return true;
return !legit.has(contract);
```
**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 `FRAX` from 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 existing `USDC`/`ETH` spoof 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 of `FRAX`, `TON`, `MUSD` and the rest entirely.
The native-asset entry is untouched: `ETH` still maps to `null`, 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:
- No `KNOWN_SYMBOLS` value names an address absent from `TOKENS` (the table is derived, and the new test would catch a hand-written entry).
- No bundled symbol normalizes differently under the spoof rule's fold than under the table's `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.
- No bundled symbol folds onto `ETH`.
- No bundled address appears twice, and no entry has a blank symbol or a malformed address.
## What was preserved
Verified unchanged, since this touches the same rule as [#235](https://git.eeqj.de/sneak/AutistMask/issues/235) and [#260](https://git.eeqj.de/sneak/AutistMask/issues/260):
- The native exemption still keys on **absence of a contract address**, not on the symbol being `ETH`, and the test adding a second null-mapped symbol still asserts it inherits both halves.
- The symbol fold (NFKC, strip what paints nothing including U+007F, trim, uppercase) is untouched, and its boundary is still pinned from both sides: the visible-controls test and the invisible-characters test both still run and both still constrain it.
- All three surfaces — `src/shared/transactions.js`, `src/popup/views/send.js`, `src/shared/balances.js` — still read the rule from `src/shared/symbolSpoof.js` with no local copies.
## Verification
- `make check`: green. 27 suites, 664 tests. `script/test-verify-build` 18/18 cases passed; prettier clean.
- `make test-e2e`: green, 27/27 in the pinned Playwright container.
- Failing-first evidence above was produced on this branch before the data-shape change, with the rest of the suite passing.
KNOWN_SYMBOLS maps a symbol to the set of contract addresses that bear
it, instead of to one of them.
A ticker is not unique, and the bundled list proves it: seven of its 512
tokens -- FRAX, REUSD, TON, EURE, MSUSD, MUSD and JPYC -- share a symbol
with another bundled entry at a different real contract. The table is
built from that list first-wins, so it kept the earlier entry of each
pair and the later one was judged a spoof of its own symbol at its own
address. A user holding any of the seven saw it filtered out of the
balance list, the transaction history and the send token selector, and
so could not spend it through the UI.
Both contracts of every pair come from the same source fetch (CoinGecko,
2026-02-27, decimals verified on-chain), so neither is stale relative to
the other and there is nothing to prefer between them. The fix is
therefore in the shape of the table rather than in its contents: no
address was picked and none was dropped. isSpoofedSymbol() asks set
membership where it asked equality, which does not loosen the rule --
every address in a set is one the wallet ships as a real token, and a
contract outside the set is still a spoof. The native-asset entry stays
null and still means no contract may bear the symbol.
The suite walked KNOWN_SYMBOLS, which is derived from TOKENS, so it
could only assert that the table agreed with itself. It now also walks
TOKENS asserting that no bundled token is filtered at its own address --
the walk that would have caught this -- pins both contracts of each of
the seven by address, asserts a third contract bearing a shared ticker
is still filtered, and asserts every address the table vouches for is a
bundled token reporting that symbol.
clawbot
self-assigned this 2026-08-12 13:15:54 +02:00
Sole defect. The change itself is correct and verified; it no longer merges.
TODO.md — conflicts with current next.next advanced to d5595c0 ("test: drive the EIP-1193 dApp approval round trips in the browser", #183) after this branch was cut at e4c3708. Both commits insert a bullet at the top of # Completed Steps.
Gitea now reports mergeable: false for this PR (it was true when opened, before d5595c0 landed).
Acceptable: rebase onto current origin/next, keeping this PR's bullet at the top of # Completed Steps above the d5595c0 bullet and losing neither. Single commit, make check and make test-e2e re-run after the rebase. No other change required.
Verified independently, all pass
Premise correction confirmed (KNOWN_SYMBOLS is derived from TOKENS at module load, first-wins; 512 tokens / 505 distinct uppercased symbols / 7 duplicated). Failing-first reproduced at e4c3708: 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 (toLowerCase on both sides); checksummed, lowercase, uppercase-hex and 0X-prefixed all match. Native ETH unfiltered at empty/null/undefined contract; legit === null returns before any .has(). Fake ETH and third contracts bearing each of the seven shared tickers still filtered, as are cross-pair queries (FRAX at the Toncoin address, USDC at the WETH address). Fold vs toUpperCase() divergence: 0 across all 512; no symbol folds onto ETH; no unreachable table key. All seven canonicality rows match the names, addresses and indices in TOKENS; 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+007F dropped -> invisible-characters test fails; build loop reverted to first-wins -> 3 new tests fail; membership check replaced by false -> 8 tests fail. Three surfaces read the rule from src/shared/symbolSpoof.js, no local copies. make check green (27 suites / 664 tests, test-verify-build 18/18, prettier clean); make test-e2e 27/27 in the pinned container — both executed here, not cached. CI green on c8e1933 (33 s). Commit hygiene, authorship, scope and terminology clean.
Disclosures
A whitespace-padded or newline-terminated contract address is judged a spoof, because normalizeAddress() lowercases without trimming. Identical on e4c3708 — pre-existing, not a regression, and it errs toward filtering. Recorded, not charged against this PR.
src/shared/tokenList.js:6 says // 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 USD is added as an address annotation in tests/symbolSpoof.test.js:387, mirroring the name field of the bundled entry. Judged data annotation rather than prose, consistent with existing occurrences in the tree; not charged.
## FAIL — `needs-rebase`
Sole defect. The change itself is correct and verified; it no longer merges.
**`TODO.md` — conflicts with current `next`.** `next` advanced to `d5595c0` ("test: drive the EIP-1193 dApp approval round trips in the browser", [#183](https://git.eeqj.de/sneak/AutistMask/issues/183)) after this branch was cut at `e4c3708`. Both commits insert a bullet at the top of `# Completed Steps`.
Reproduction:
```
$ git fetch origin
$ git merge-base --is-ancestor origin/next c8e1933; echo $?
1
$ git checkout -B t origin/next && git merge --no-commit --no-ff c8e1933
Auto-merging TODO.md
CONFLICT (content): Merge conflict in TODO.md
```
Gitea now reports `mergeable: false` for this PR (it was `true` when opened, before `d5595c0` landed).
Acceptable: rebase onto current `origin/next`, keeping this PR's bullet at the top of `# Completed Steps` above the `d5595c0` bullet and losing neither. Single commit, `make check` and `make test-e2e` re-run after the rebase. No other change required.
### Verified independently, all pass
Premise correction confirmed (`KNOWN_SYMBOLS` is derived from `TOKENS` at module load, first-wins; 512 tokens / 505 distinct uppercased symbols / 7 duplicated). Failing-first reproduced at `e4c3708`: 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 (`toLowerCase` on both sides); checksummed, lowercase, uppercase-hex and `0X`-prefixed all match. Native `ETH` unfiltered at empty/`null`/`undefined` contract; `legit === null` returns before any `.has()`. Fake `ETH` and third contracts bearing each of the seven shared tickers still filtered, as are cross-pair queries (`FRAX` at the Toncoin address, `USDC` at the WETH address). Fold vs `toUpperCase()` divergence: 0 across all 512; no symbol folds onto `ETH`; no unreachable table key. All seven canonicality rows match the names, addresses and indices in `TOKENS`; 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+007F` dropped -> invisible-characters test fails; build loop reverted to first-wins -> 3 new tests fail; membership check replaced by `false` -> 8 tests fail. Three surfaces read the rule from `src/shared/symbolSpoof.js`, no local copies. `make check` green (27 suites / 664 tests, `test-verify-build` 18/18, prettier clean); `make test-e2e` 27/27 in the pinned container — both executed here, not cached. CI green on `c8e1933` (33 s). Commit hygiene, authorship, scope and terminology clean.
### Disclosures
- A whitespace-padded or newline-terminated contract address is judged a spoof, because `normalizeAddress()` lowercases without trimming. Identical on `e4c3708` — pre-existing, not a regression, and it errs toward filtering. Recorded, not charged against this PR.
- `src/shared/tokenList.js:6` says `// 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 USD` is added as an address annotation in `tests/symbolSpoof.test.js:387`, mirroring the `name` field of the bundled entry. Judged data annotation rather than prose, consistent with existing occurrences in the tree; not charged.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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