Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion keepkeylib/eth/ethereum_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,26 @@ def build(self):
self.add_tokens(network)

def serialize_c(self, outf):
for token in sorted(self.tokens, key=lambda t: t.token['address']):
# Flash budget: this table is the largest read-only symbol in the ARM
# image. See token_policy for why it is capped rather than complete.
# Run as a standalone script by the build, so there is no package
# context for a relative import.
import os as _os, sys as _s
_s.path.insert(0, _os.path.dirname(_os.path.realpath(__file__)))
import token_policy
chosen, ambiguous = token_policy.select(
self.tokens,
token_policy.BUDGET_ETHEREUM_LISTS,
symbol_of=lambda t: t.token.get('symbol', ''),
address_of=lambda t: t.token['address'].lower())
print('ethereum_tokens: %d of %d kept (budget %d)'
% (len(chosen), len(self.tokens),
token_policy.BUDGET_ETHEREUM_LISTS), file=sys.stderr)
if ambiguous:
print('ethereum_tokens: priority symbols DROPPED as ambiguous '
'(>1 address, a scam token can inherit a real label): %s'
% ', '.join(sorted(ambiguous)), file=sys.stderr)
for token in sorted(chosen, key=lambda t: t.token['address']):
token.serialize_c(outf)

def is_ascii(s):
Expand Down
124 changes: 124 additions & 0 deletions keepkeylib/eth/token_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Which ERC-20s earn their place in firmware flash.

The built-in token table is the single largest read-only symbol in the ARM
image -- 31,104 bytes of `tokens` for 1,945 entries, larger than MessagesMap or
the BIP-39 wordlist. It exists so the device can render "10.5 DAI" instead of a
raw amount against a bare contract address.

It cannot be complete, and should not try to be. Two facts settle that:

* The vetted source (ethereum-lists) is a SNAPSHOT and is stale. It has no
UNI, no AAVE, no stETH, no PEPE, none of the modern stables (FRAX, PYUSD,
crvUSD, USDe), and its `ARB` entry is a 2018 token called "ARBITRAGE", not
Arbitrum's. Shipping 1,945 entries does not make the table current; it
makes it 1,945 entries of mostly-2018 long tail.
* Anything outside the table is not undisplayable -- it is the clear-sign
provider's job, which is exactly the direction
docs/security/token-table-retirement.md sets out.

So the table's job is narrow: the assets a user is most likely to hold, whose
addresses this repository can actually vouch for. Everything else is a provider
schema away.

POLICY
1. A budget, because flash is finite and this symbol is the biggest one.
2. Priority symbols first -- stablecoins, then majors.
3. A priority symbol is only taken when the vetted source gives it exactly
ONE address. Two entries sharing a symbol is how a scam token inherits a
real one's label, and the device would render the attacker's name.
4. Remaining budget filled in the existing deterministic order (by address),
so the result is reproducible and diffable.

Addresses are NEVER written here. They come from the vetted source, matched by
symbol. A hand-typed address in a token table is a mislabelling defect waiting
to happen, and this file must not become the place one appears.
"""

# 500 entries * 16 bytes = ~8 KB, against 31 KB today.
TOKEN_BUDGET = 500

# Split across the two generators, which emit into one array.
BUDGET_ETHEREUM_LISTS = 350
BUDGET_UNISWAP_LIST = 150

STABLECOINS = [
"USDC", "USDT", "DAI", "TUSD", "BUSD", "USDP", "GUSD", "SAI",
"EURS", "EURT", "sUSD", "USDS", "FRAX", "LUSD", "PYUSD", "crvUSD", "USDe",
]

MAJORS = [
"WETH", "WBTC", "stETH", "wstETH", "rETH", "cbETH", "LINK", "UNI", "AAVE",
"MKR", "LDO", "CRV", "SNX", "COMP", "ENS", "GRT", "MATIC", "ARB", "OP",
"SHIB", "PEPE", "APE", "SAND", "MANA", "AXS", "IMX", "INJ", "RNDR", "FET",
"STG", "BAL", "1INCH", "SUSHI", "YFI", "BAT", "ZRX", "KNC", "LRC", "GNO",
"RPL", "FXS", "CVX", "PAXG", "AMPL", "OMG", "REP", "ZIL", "ENJ", "STORJ",
"GUSD",
]

# Required by coins[] in the firmware, not by popularity. Each of these is a
# display-only entry in the device's own coin table carrying a contract
# address, and unittests/firmware/coins.cpp (Coins.TableSanity) asserts every
# one of them resolves UNIQUELY in this token table. Dropping any is a build
# failure, correctly: the device would advertise a coin it cannot name.
#
# They are overwhelmingly 2017-era ICO tokens and are exactly the long tail
# this budget exists to cut -- but the cut has to happen in coins[] first, and
# coins[] is itself a 23,808-byte symbol. That is the next reduction, not this
# one. See docs/security/token-table-retirement.md.
REQUIRED_BY_COINS = [
"0xBTC", "1ST", "AE", "ANT", "CVC", "DGD", "ELF", "FOX", "FUN", "GNT",
"GUP", "ICN", "MLN", "MTL", "PAY", "POLY", "PPT", "RCN", "RLC", "SALT",
"SNGLS", "SNT", "SPANK", "SWT", "TRST", "WINGS",
]

# Required by a TEST FIXTURE rather than by the product. ADT (AdToken) is a
# 2017 ICO token that test_ethereum_signtx_knownerc20_eip_1559 uses as its
# canonical "known ERC-20", asserting a hardcoded signature over a transfer to
# its address -- so dropping it fails the suite, and the fixture cannot be
# repointed at a current token without regenerating that signature.
#
# It is listed separately and deliberately: a fixture should not get to pin
# firmware flash. Migrating that test to USDC (which every user actually holds)
# retires this entry, and is tracked as fixture debt rather than done here,
# because changing a signature fixture is a change to what the test proves.
REQUIRED_BY_TESTS = ["ADT"]

PRIORITY_SYMBOLS = (REQUIRED_BY_COINS + REQUIRED_BY_TESTS
+ STABLECOINS + MAJORS)


def select(records, budget, symbol_of, address_of):
"""Return `records` trimmed to `budget`, priority symbols first.

`records` is any iterable; `symbol_of`/`address_of` pull the two fields.
Priority symbols with more than one address in `records` are DROPPED from
the priority pass -- see rule 3 -- though they may still be picked up by
the deterministic fill, where they carry no special standing.
"""
records = list(records)
by_symbol = {}
for r in records:
by_symbol.setdefault(symbol_of(r), []).append(r)

chosen, seen = [], set()
ambiguous = []
for sym in PRIORITY_SYMBOLS:
hits = by_symbol.get(sym, [])
if len(hits) > 1:
ambiguous.append(sym)
continue
for r in hits:
key = address_of(r)
if key not in seen:
seen.add(key)
chosen.append(r)

for r in sorted(records, key=address_of):
if len(chosen) >= budget:
break
key = address_of(r)
if key not in seen:
seen.add(key)
chosen.append(r)

return chosen[:budget], ambiguous
20 changes: 19 additions & 1 deletion keepkeylib/eth/uniswap_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,26 @@ def build(self):
self.ustoks.append(USETHToken(token))

def serialize_c(self):
# Flash budget -- see token_policy.
# Run as a standalone script by the build, so there is no package
# context for a relative import.
import os as _os, sys as _s
_s.path.insert(0, _os.path.dirname(_os.path.realpath(__file__)))
import token_policy
import sys as _sys
chosen, ambiguous = token_policy.select(
self.ustoks,
token_policy.BUDGET_UNISWAP_LIST,
symbol_of=lambda t: t.token.get('symbol', ''),
address_of=lambda t: t.token['contractAddress'].lower())
print('uniswap_tokens: %d of %d kept (budget %d)'
% (len(chosen), len(self.ustoks),
token_policy.BUDGET_UNISWAP_LIST), file=_sys.stderr)
if ambiguous:
print('uniswap_tokens: priority symbols DROPPED as ambiguous: %s'
% ', '.join(sorted(ambiguous)), file=_sys.stderr)
ser_list = []
for token in sorted(self.ustoks, key=lambda t: t.token['contractAddress']):
for token in sorted(chosen, key=lambda t: t.token['contractAddress']):
ser_list.append(token.serialize_c())
return(ser_list)

Expand Down
Loading