|
| 1 | +"""Which ERC-20s earn their place in firmware flash. |
| 2 | +
|
| 3 | +The built-in token table is the single largest read-only symbol in the ARM |
| 4 | +image -- 31,104 bytes of `tokens` for 1,945 entries, larger than MessagesMap or |
| 5 | +the BIP-39 wordlist. It exists so the device can render "10.5 DAI" instead of a |
| 6 | +raw amount against a bare contract address. |
| 7 | +
|
| 8 | +It cannot be complete, and should not try to be. Two facts settle that: |
| 9 | +
|
| 10 | + * The vetted source (ethereum-lists) is a SNAPSHOT and is stale. It has no |
| 11 | + UNI, no AAVE, no stETH, no PEPE, none of the modern stables (FRAX, PYUSD, |
| 12 | + crvUSD, USDe), and its `ARB` entry is a 2018 token called "ARBITRAGE", not |
| 13 | + Arbitrum's. Shipping 1,945 entries does not make the table current; it |
| 14 | + makes it 1,945 entries of mostly-2018 long tail. |
| 15 | + * Anything outside the table is not undisplayable -- it is the clear-sign |
| 16 | + provider's job, which is exactly the direction |
| 17 | + docs/security/token-table-retirement.md sets out. |
| 18 | +
|
| 19 | +So the table's job is narrow: the assets a user is most likely to hold, whose |
| 20 | +addresses this repository can actually vouch for. Everything else is a provider |
| 21 | +schema away. |
| 22 | +
|
| 23 | +POLICY |
| 24 | + 1. A budget, because flash is finite and this symbol is the biggest one. |
| 25 | + 2. Priority symbols first -- stablecoins, then majors. |
| 26 | + 3. A priority symbol is only taken when the vetted source gives it exactly |
| 27 | + ONE address. Two entries sharing a symbol is how a scam token inherits a |
| 28 | + real one's label, and the device would render the attacker's name. |
| 29 | + 4. Remaining budget filled in the existing deterministic order (by address), |
| 30 | + so the result is reproducible and diffable. |
| 31 | +
|
| 32 | +Addresses are NEVER written here. They come from the vetted source, matched by |
| 33 | +symbol. A hand-typed address in a token table is a mislabelling defect waiting |
| 34 | +to happen, and this file must not become the place one appears. |
| 35 | +""" |
| 36 | + |
| 37 | +# 500 entries * 16 bytes = ~8 KB, against 31 KB today. |
| 38 | +TOKEN_BUDGET = 500 |
| 39 | + |
| 40 | +# Split across the two generators, which emit into one array. |
| 41 | +BUDGET_ETHEREUM_LISTS = 350 |
| 42 | +BUDGET_UNISWAP_LIST = 150 |
| 43 | + |
| 44 | +STABLECOINS = [ |
| 45 | + "USDC", "USDT", "DAI", "TUSD", "BUSD", "USDP", "GUSD", "SAI", |
| 46 | + "EURS", "EURT", "sUSD", "USDS", "FRAX", "LUSD", "PYUSD", "crvUSD", "USDe", |
| 47 | +] |
| 48 | + |
| 49 | +MAJORS = [ |
| 50 | + "WETH", "WBTC", "stETH", "wstETH", "rETH", "cbETH", "LINK", "UNI", "AAVE", |
| 51 | + "MKR", "LDO", "CRV", "SNX", "COMP", "ENS", "GRT", "MATIC", "ARB", "OP", |
| 52 | + "SHIB", "PEPE", "APE", "SAND", "MANA", "AXS", "IMX", "INJ", "RNDR", "FET", |
| 53 | + "STG", "BAL", "1INCH", "SUSHI", "YFI", "BAT", "ZRX", "KNC", "LRC", "GNO", |
| 54 | + "RPL", "FXS", "CVX", "PAXG", "AMPL", "OMG", "REP", "ZIL", "ENJ", "STORJ", |
| 55 | + "GUSD", |
| 56 | +] |
| 57 | + |
| 58 | +# Required by coins[] in the firmware, not by popularity. Each of these is a |
| 59 | +# display-only entry in the device's own coin table carrying a contract |
| 60 | +# address, and unittests/firmware/coins.cpp (Coins.TableSanity) asserts every |
| 61 | +# one of them resolves UNIQUELY in this token table. Dropping any is a build |
| 62 | +# failure, correctly: the device would advertise a coin it cannot name. |
| 63 | +# |
| 64 | +# They are overwhelmingly 2017-era ICO tokens and are exactly the long tail |
| 65 | +# this budget exists to cut -- but the cut has to happen in coins[] first, and |
| 66 | +# coins[] is itself a 23,808-byte symbol. That is the next reduction, not this |
| 67 | +# one. See docs/security/token-table-retirement.md. |
| 68 | +REQUIRED_BY_COINS = [ |
| 69 | + "0xBTC", "1ST", "AE", "ANT", "CVC", "DGD", "ELF", "FOX", "FUN", "GNT", |
| 70 | + "GUP", "ICN", "MLN", "MTL", "PAY", "POLY", "PPT", "RCN", "RLC", "SALT", |
| 71 | + "SNGLS", "SNT", "SPANK", "SWT", "TRST", "WINGS", |
| 72 | +] |
| 73 | + |
| 74 | +# Required by a TEST FIXTURE rather than by the product. ADT (AdToken) is a |
| 75 | +# 2017 ICO token that test_ethereum_signtx_knownerc20_eip_1559 uses as its |
| 76 | +# canonical "known ERC-20", asserting a hardcoded signature over a transfer to |
| 77 | +# its address -- so dropping it fails the suite, and the fixture cannot be |
| 78 | +# repointed at a current token without regenerating that signature. |
| 79 | +# |
| 80 | +# It is listed separately and deliberately: a fixture should not get to pin |
| 81 | +# firmware flash. Migrating that test to USDC (which every user actually holds) |
| 82 | +# retires this entry, and is tracked as fixture debt rather than done here, |
| 83 | +# because changing a signature fixture is a change to what the test proves. |
| 84 | +REQUIRED_BY_TESTS = ["ADT"] |
| 85 | + |
| 86 | +PRIORITY_SYMBOLS = (REQUIRED_BY_COINS + REQUIRED_BY_TESTS |
| 87 | + + STABLECOINS + MAJORS) |
| 88 | + |
| 89 | + |
| 90 | +def select(records, budget, symbol_of, address_of): |
| 91 | + """Return `records` trimmed to `budget`, priority symbols first. |
| 92 | +
|
| 93 | + `records` is any iterable; `symbol_of`/`address_of` pull the two fields. |
| 94 | + Priority symbols with more than one address in `records` are DROPPED from |
| 95 | + the priority pass -- see rule 3 -- though they may still be picked up by |
| 96 | + the deterministic fill, where they carry no special standing. |
| 97 | + """ |
| 98 | + records = list(records) |
| 99 | + by_symbol = {} |
| 100 | + for r in records: |
| 101 | + by_symbol.setdefault(symbol_of(r), []).append(r) |
| 102 | + |
| 103 | + chosen, seen = [], set() |
| 104 | + ambiguous = [] |
| 105 | + for sym in PRIORITY_SYMBOLS: |
| 106 | + hits = by_symbol.get(sym, []) |
| 107 | + if len(hits) > 1: |
| 108 | + ambiguous.append(sym) |
| 109 | + continue |
| 110 | + for r in hits: |
| 111 | + key = address_of(r) |
| 112 | + if key not in seen: |
| 113 | + seen.add(key) |
| 114 | + chosen.append(r) |
| 115 | + |
| 116 | + for r in sorted(records, key=address_of): |
| 117 | + if len(chosen) >= budget: |
| 118 | + break |
| 119 | + key = address_of(r) |
| 120 | + if key not in seen: |
| 121 | + seen.add(key) |
| 122 | + chosen.append(r) |
| 123 | + |
| 124 | + return chosen[:budget], ambiguous |
0 commit comments