diff --git a/.changeset/base-sepolia-usdt-decimals.md b/.changeset/base-sepolia-usdt-decimals.md new file mode 100644 index 0000000..b83928f --- /dev/null +++ b/.changeset/base-sepolia-usdt-decimals.md @@ -0,0 +1,5 @@ +--- +"@avaprotocol/protocols": patch +--- + +Correct Base Sepolia `USDT` (`0xCE8565457Cca0fC7542608A2c78610Ed7bC66C8C`) to 18 decimals. The row said 6, but the contract's `decimals()` returns 18, so amounts formatted from the catalog were off by 10^12. The contract is an unverified third-party test token, not a Tether deployment. Add `yarn verify:tokens`, a read-only check of every catalog row's code, `decimals()` and `symbol()` against the chain. diff --git a/.changeset/bnb-chain-baseline-tokens.md b/.changeset/bnb-chain-baseline-tokens.md new file mode 100644 index 0000000..3aa9a2f --- /dev/null +++ b/.changeset/bnb-chain-baseline-tokens.md @@ -0,0 +1,7 @@ +--- +"@avaprotocol/protocols": minor +--- + +Add the BNB Chain baseline to the token catalog: WBNB, Binance-Peg BTCB and ETH, FDUSD, USD1, USDe, sUSDe, Cake, wstETH, slisBNB, LBTC, SolvBTC, XVS, LINK, and BNBx (legacy: Stader discontinued it). Each address was checked against the issuer's or BNB Chain's published list and on-chain. + +`lookupToken(56, WBNB)` now returns `WBNB`. Before, it fell through to `Protocols.uniswapV3.tokens.WETH`, whose BNB Chain entry holds WBNB, and answered with symbol `"WETH"`. BNB Chain's `ETH` symbol is the Binance-Peg ERC-20, not native ETH. diff --git a/.changeset/ethereum-base-baseline-tokens.md b/.changeset/ethereum-base-baseline-tokens.md new file mode 100644 index 0000000..8659b7e --- /dev/null +++ b/.changeset/ethereum-base-baseline-tokens.md @@ -0,0 +1,10 @@ +--- +"@avaprotocol/protocols": minor +--- + +Fill the Ethereum and Base token-catalog gaps found by the coverage audit (#38). + +- Ethereum: RLUSD, USDG, GHO, cbETH, sDAI, crvUSD, LUSD, USDtb, eUSDe, SKY, BAL, 1INCH, FXS, KNC, STG, ETHx, ezETH, tETH, frxETH, sfrxETH, FBTC, eBTC, BTC.b, mUSD and syrupUSDT. +- Base: RLUSD (`0x8d58C0C60B8D6b88Fa98B291a646dB34d0F98258`, Ripple's L2 address), WBTC, GHO, LBTC, tBTC, weETH, ezETH, wrsETH, syrupUSDC, USDS, sUSDS, USDe and sUSDe. + +Every Aave V3 Core reserve on Ethereum, Base and BNB Chain (except expiring Pendle PTs) is now in `Tokens`, so `lookupToken` recognizes them without the per-protocol fallback. diff --git a/.changeset/rename-solvbtc-bbn-xsolvbtc.md b/.changeset/rename-solvbtc-bbn-xsolvbtc.md new file mode 100644 index 0000000..0724807 --- /dev/null +++ b/.changeset/rename-solvbtc-bbn-xsolvbtc.md @@ -0,0 +1,7 @@ +--- +"@avaprotocol/protocols": major +--- + +Rename Ethereum `Tokens["SolvBTC.BBN"]` to `Tokens.xSolvBTC` (`0xd9D920AA40f578ab794426F5C90F6C731D159DEf`). Solv rebranded the token: the contract's `name()` and `symbol()` both return `xSolvBTC`. The address and decimals are unchanged, and `lookupToken` now returns symbol and name `xSolvBTC` for it. + +BREAKING CHANGE: `Tokens["SolvBTC.BBN"]` no longer exists, and the Ethereum sidecar (`dist/tokens/ethereum.json`) carries `xSolvBTC` for this address. Callers keying on the old symbol must switch to `xSolvBTC`. diff --git a/.changeset/token-provenance-coverage.md b/.changeset/token-provenance-coverage.md new file mode 100644 index 0000000..7169cda --- /dev/null +++ b/.changeset/token-provenance-coverage.md @@ -0,0 +1,10 @@ +--- +"@avaprotocol/protocols": minor +--- + +Record transfer constraints on token rows and pin catalog addresses to their sources. + +- `TokenChainEntry.transferConstraints` lists behaviour that makes a plain `transfer` to an arbitrary address revert or deliver a different amount. Values come from the new `TOKEN_TRANSFER_CONSTRAINT`: `permissionedRecipient` (OUSG, BUIDL, USYC), `rebasing` (stETH, eETH) and `feeOnTransfer` (none listed). No constraints means none is known, not that every transfer succeeds. +- Provenance for each row (canonical sources, native/bridged/test, verification date) lives in `src/tokens/provenance/*.json`. Tests read it; the package does not ship it. +- `tests/token-coverage.test.ts` enforces a defined coverage target per chain, address agreement between provenance, `Tokens` and protocol modules, and one symbol per address across chains. +- `docs/TOKEN_CATALOG.md` documents the inclusion and maintenance policy and how consumers should separate recognizing a holding from deciding whether it can be transferred. `docs/TOKEN_COVERAGE.md` is the generated per-chain matrix (`yarn report:tokens`). diff --git a/README.md b/README.md index 3da744e..298d207 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,21 @@ There's no equivalent multi-protocol + multi-chain + addresses + ABIs + topics p Address verification: link to the canonical source (Etherscan-verified deployment, protocol docs, official address book). PRs that don't cite a source for the addresses won't merge. +## Token catalog + +`Tokens` recognizes assets by `(chainId, address)`: identity, decimals and who issues them. It promises a defined coverage target per chain rather than "all common tokens". A listed token is not a promise that every transfer succeeds; rows carry `transferConstraints` where a plain transfer can revert or deliver less. + +- Policy, coverage rules and consumer guidance: [docs/TOKEN_CATALOG.md](./docs/TOKEN_CATALOG.md) +- Generated per-chain matrix: [docs/TOKEN_COVERAGE.md](./docs/TOKEN_COVERAGE.md) + +Adding or changing a token row means three files: the row in `src/tokens/data/.json`, its sources in `src/tokens/provenance/.json`, and the regenerated matrix: + +```bash +yarn verify:tokens --rows candidates.json # read-only on-chain check of new rows +yarn report:tokens # regenerate docs/TOKEN_COVERAGE.md +yarn verify:tokens # check every row; run before a release +``` + ## Tokens sidecar (for non-TS consumers) `yarn build` writes a per-chain JSON file under `dist/tokens/` alongside the JS + d.ts outputs: diff --git a/docs/TOKEN_CATALOG.md b/docs/TOKEN_CATALOG.md new file mode 100644 index 0000000..aa76b20 --- /dev/null +++ b/docs/TOKEN_CATALOG.md @@ -0,0 +1,107 @@ +# Token catalog + +`Tokens` (and the per-chain `@avaprotocol/protocols/tokens/` modules) answers one question: **is this contract the asset it claims to be, on this chain?** A listed row is a verified identity — address, decimals, symbol, and who issues it. It is not an investment endorsement, and it is not a promise that every transfer of the token succeeds. + +The per-chain coverage matrix is generated: [TOKEN_COVERAGE.md](./TOKEN_COVERAGE.md). + +## Coverage target + +"All common tokens" cannot be tested, so the catalog promises an explicit list instead. It lives in `scripts/token-catalog/coverage.ts`, and `tests/token-coverage.test.ts` fails when a target asset is neither listed nor recorded as a known gap. + +For each supported mainnet (Ethereum, Base, BNB Chain): + +1. **Native wrapper** — the canonical wrapper of the native coin, the address `Protocols.wrapped.weth[chainId]` names (WETH, WBNB). +2. **Fiat stablecoins** — USDC, EURC, USDT, DAI, USDS, USDe, PYUSD, RLUSD, USDG, FDUSD and USD1, on each chain where the issuer itself publishes an address. Where the widely used variant is bridged (Binance-Peg USDC/USDT on BNB Chain, bridged USDT on Base), the bridged variant is listed and labelled as such. +3. **BTC representations** — WBTC, cbBTC, tBTC, LBTC (BTCB on BNB Chain), where the issuer publishes an address and the deployment has supply. +4. **Liquid staking** — stETH/wstETH, rETH, cbETH, weETH (slisBNB on BNB Chain), where the issuer publishes an address. +5. **Established DeFi assets** — every Aave V3 Core reserve on the chain, matched by address, except expiring Pendle principal tokens; plus the chain's main DEX governance token (UNI, AERO, Cake). Aave governance lists only liquid, vetted assets, and `Protocols.aaveV3.reserves` is regenerated from chain, so a new Aave listing surfaces as a coverage failure. + +For testnets (Sepolia, Base Sepolia): + +6. **Verified test deployments only** — WETH, Circle's test USDC and EURC, and Chainlink's LINK. A mainnet asset does not imply a test token. Protocol faucet tokens (Aave's test USDC/DAI/WBTC…) collide with issuer test tokens by symbol and stay in their protocol module. + +Out of scope on purpose: native coins (ETH, BNB) — they are not ERC-20 contracts and consumers handle them separately; expiring tokens (Pendle PTs); assets whose issuer stopped issuing and that see no use (BUSD). Chains in `Chains` without a token file (Optimism, Arbitrum, Unichain, Polygon, Hyperliquid, Robinhood) carry protocol addresses only; adding one means a new data file, a provenance file, and a coverage target. + +Assets outside the target may still be listed — the catalog also carries rows ported from Studio's earlier lists — but only the target is promised. + +## Adding or changing a row + +1. **Find a canonical source.** The issuer's or protocol's own address page, official docs, or official repository, listing this exact address on this chain. Explorer labels, CoinGecko and aggregator lists (PancakeSwap, Uniswap, Superchain) are secondary evidence: record them, but do not rely on them alone. Symbol and name are not evidence. A match found through `lookupToken`'s cross-chain fallback is not evidence either: the same 20 bytes can be a different contract, or nothing, on another chain. +2. **Check the chain.** Put the candidate in a JSON array of `{ chainId, symbol, address, decimals }` and run `yarn verify:tokens --rows candidates.json`. It reads code, `decimals()` and `symbol()`; a decimals mismatch fails. A malformed row, or one on a chain without a catalog file, fails the run before anything is read. +3. **Classify the deployment.** Native (minted on this chain by the issuer), bridged (name the bridge or operator — "Binance-Peg", "Base standard bridge", "LayerZero OFT"), or test (name who deployed it). Mark superseded deployments `legacy`. +4. **Check transfers.** Read the issuer's docs for allowlists, rebasing and transfer fees. Where useful, simulate a full-balance `transfer` from a current holder to a fresh address with `eth_call` and a state override — read-only, no signatures — and compare what was sent with what arrived. Record any constraint in `transferConstraints` (below). +5. **Edit three files.** The row in `src/tokens/data/.json`, its provenance in `src/tokens/provenance/.json` (sources, references, `verifiedAt`, notes), and the regenerated matrix (`yarn report:tokens`). Add a changeset. + +Symbols follow the issuer's on-chain casing (`stETH`, `Cake`, `cbBTC`). A symbol appears once per chain. An address names one asset on every chain where it appears, because `lookupToken` falls back across chains; the test enforces this. + +Symbol keys are API: consumers index `Tokens` by them, and the sidecar carries them. When an issuer rebrands on-chain, follow it (Ethereum `SolvBTC.BBN` became `xSolvBTC`), ship the rename as a major changeset, and record the old symbol in the provenance notes. + +## Maintenance + +- **Before a release**, run `yarn verify:tokens`. It needs network access, so it is not part of `yarn test`. A scheduled CI job running it weekly would catch address drift between releases; it is proposed, not yet added. +- **After regenerating Aave reserves** (`yarn generate:aave-reserves`), the coverage test lists any new Core reserve. Add it, or record a known gap with a reason. +- **`KNOWN_GAPS`** in `scripts/token-catalog/coverage.ts` is the explicit list of target assets not yet listed. The test fails when a gap is filled but still listed. +- **`PROVENANCE_PENDING`** lists rows that predate provenance tracking. It may only shrink: a new row needs a provenance row, and a pending row that gains provenance must leave the list. +- **Removing a row** breaks consumers that key on it. Prefer marking it `legacy` in provenance; remove only addresses that are wrong. + +## Transfer constraints + +`TokenChainEntry.transferConstraints` (values in `TOKEN_TRANSFER_CONSTRAINT`) records behaviour that makes a plain `transfer(to, amount)` to an arbitrary address revert or deliver a different amount: + +| Constraint | Meaning | Listed today | +|---|---|---| +| `permissionedRecipient` | The recipient must be on the issuer's allowlist (KYC, qualified access). A transfer to a fresh address reverts — including the holder's own new wallet. | OUSG, BUIDL, USYC (Ethereum) | +| `rebasing` | Share-based balances. A full-balance transfer can leave 1–2 wei behind, and the amount received can differ from the amount requested by rounding. | stETH, eETH (Ethereum) | +| `feeOnTransfer` | The recipient receives less than the amount sent. | none (FLOKI taxes only trades through registered exchange pools; wallet-to-wallet transfers are free) | + +What is deliberately not recorded: issuer freezes, pauses and sanctions blocklists. Almost every fiat-backed token has them, so flagging them would flag everything and tell a consumer nothing. + +No constraints means none is known — not that every transfer succeeds. + +## Guidance for consumers + +**Recognition is not transfer eligibility.** Use the catalog to recognize holdings: name, decimals, logo, value. Decide whether a transfer works when you prepare it, by simulating the actual transfers. + +- **Show recognized holdings, including constrained ones.** A wallet holding OUSG holds OUSG. Do not hide it because it has extra transfer rules. +- **Flag, then validate.** For a batch (Studio's Sweep), surface `transferConstraints` on the selection step, and let preparation simulate each transfer to the chosen recipient. A `permissionedRecipient` token to an address the issuer has not onboarded will revert; exclude it from the atomic batch with an explanation rather than failing the whole batch. +- **Rebasing tokens:** read the balance at preparation time, and expect 1–2 wei to remain after a "full" transfer. Do not treat that dust as a failure. +- **A symbol is not an identity.** BNB Chain's `ETH` is the Binance-Peg ERC-20, not native ETH. Sepolia's `USDT` is Aave's faucet token, not Tether's. Never infer "native coin" from a symbol; key everything on `(chainId, address)`. +- **Pass the chain.** `lookupToken(chainId, address)` checks that chain first. Its cross-chain fallback exists for dev gateways whose runtime chain differs from the workflow's; do not treat a fallback match as proof of what the contract is on your chain. +- **Unlisted is not unsafe.** The catalog promises its coverage target, nothing more. Show unknown ERC-20s as unrecognized rather than dropping them. + +## Audit record — 2026-10-28 (issue #38) + +Starting point: PR #37's head, 143 rows (Ethereum 105, Base 24, BNB Chain 2, Sepolia 7, Base Sepolia 5). + +**Method.** Every row was read on-chain (`yarn verify:tokens`). Each proposed addition was checked against the issuer's or protocol's published address page and on-chain before it was added. Transfer behaviour was checked two ways: issuer documentation and verified contract source, and a read-only `eth_call` that installs a probe at a current holder's address (state override) and simulates a full-balance `transfer` to a fresh address. Every mainnet row was simulated except seven for which public endpoints showed no externally owned holder: Base BAL and WBTC; BNB Chain wstETH, sUSDe, XVS, LBTC and LINK. No transaction was sent and nothing was signed. + +**Findings, fixed here** + +- Base Sepolia `USDT` said 6 decimals; the contract returns 18. It is an unverified third-party test token (40 holders), not Tether's. EigenLayer-AVS embeds the same wrong value in `core/taskengine/tokenwhitelist/base-sepolia.json`; its drift check against the sidecar should flag it once this ships. +- `lookupToken(56, WBNB)` returned symbol `"WETH"` through `Protocols.uniswapV3.tokens.WETH`, whose BNB Chain entry holds WBNB. WBNB is now catalogued. +- BNB Chain listed two tokens; it now covers its baseline (17 rows). +- 25 Ethereum and 13 Base rows added, including RLUSD on both chains. Every Aave V3 Core reserve on the three mainnets is now in `Tokens` by address. +- Ethereum `SolvBTC.BBN` is now `xSolvBTC`. Solv rebranded the token: the contract's `name()` and `symbol()` both return `xSolvBTC`, and Solv's repository names this address xSolvBTC. The old key no longer exists. + +**Findings, recorded** + +- Sepolia `USDT` is Aave V3's faucet token, not Tether's (Tether publishes no Sepolia USDT). Sepolia `UNI` was deployed by mainnet UNI's deployer. Sepolia `DAI` and Base Sepolia `USDT` have no attributable issuer; they stay listed, marked pending. +- PAXG no longer charges a transfer fee: its current implementation (PaxosTokenV2, `0x7da4…a543`) has no fee logic and `feeRate()` reverts. +- USDtb's current implementation has a blocklist and pause only; the old whitelist transfer state is deprecated. USDY's allowlist contract returns true for every address. LsETH's allowlist gates deposits and redemptions, not transfers. Maple's syrupUSDC has no transfer permission set today. Issuers can change all of these by upgrade, which is why consumers must still simulate. +- Legacy or winding down, kept for existing holdings: MKR (superseded by SKY), Base USDbC (bridged USDC from before native USDC), BNBx (Stader discontinued BNB staking), and — on secondary evidence only — cmETH (minting reportedly disabled 2026-05-07), legacy FRAX, Ethereum's 2017 ERC-20 BNB, and Wrapped TON (its bridge reportedly closed 2026-09-01). +- Base `weETH`: the Superchain token list carries a different address (`0x4c94…5EFA`) that ether.fi does not list. The catalog follows ether.fi. + +**Remaining gaps.** `KNOWN_GAPS` is empty: every target asset is listed. Outside the target, and not listed: + +- WBTC on BNB Chain: BitGo lists `0x3966…a86f` with zero supply. The PancakeSwap list's `0x0555…2B9c` is not on BitGo's list. +- SolvBTC on Base (`0x3B86…931f`, from Solv's deploy script) and MORPHO on Ethereum: not yet verified. +- Aave's non-Core Ethereum markets (EtherFi, Lido, Horizon) list reserves the target does not cover. +- Chains without a token file: Optimism, Arbitrum, Unichain, Polygon, Hyperliquid, Robinhood. +- 80 pre-audit rows still lack a recorded canonical source (`PROVENANCE_PENDING`: Ethereum 67, Base 11, Sepolia 1, Base Sepolia 1). Among them, OUSG, BUIDL and USYC carry transfer constraints. + +**Follow-ups proposed** + +- A weekly CI job running `yarn verify:tokens`, opening an issue on drift. +- Work down `PROVENANCE_PENDING`, starting with the stablecoins and RWA tokens (USDY, USD0, TUSD, OUSG, BUIDL, USYC) and the Base standard-bridge tokens, whose bridge pointers are already confirmed on-chain. +- Studio: after the release, drop the EURC compatibility rows in `app/lib/erc20/catalog.ts` (studio#2028), and surface `transferConstraints` on the Sweep selection step. + diff --git a/docs/TOKEN_COVERAGE.md b/docs/TOKEN_COVERAGE.md new file mode 100644 index 0000000..b7fae6c --- /dev/null +++ b/docs/TOKEN_COVERAGE.md @@ -0,0 +1,218 @@ +# Token catalog coverage + + + +Per-chain coverage of the shared token catalog against the target in `scripts/token-catalog/coverage.ts`. Selection rules, status definitions and the maintenance policy are in [TOKEN_CATALOG.md](./TOKEN_CATALOG.md). + +Status: **present** (listed, canonical source recorded); **bridged** or **test token** (listed variant, with who issues it); **legacy** (superseded deployment kept for existing holdings); **unverified provenance** (listed, on-chain checked, no canonical source recorded); **transfer handling** (listed with transfer constraints); **missing** (in the target, not listed yet). + +## Summary + +| Chain | Rows | Provenance recorded | Provenance pending | Known gaps | With transfer constraints | +|---|---|---|---|---|---| +| Ethereum | 130 | 63 | 67 | 0 | 5 | +| Base | 37 | 26 | 11 | 0 | 0 | +| BNB Chain | 17 | 17 | 0 | 0 | 0 | +| Sepolia | 7 | 6 | 1 | 0 | 0 | +| Base Sepolia | 5 | 4 | 1 | 0 | 0 | + +## Ethereum (chain 1) + +### Baseline + +| Asset | Category | Status | Address | Sources | Verified | +|---|---|---|---|---|---| +| WETH | Native wrapper | present | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | [ethereum.org](https://ethereum.org/en/wrapped-eth/), [developers.uniswap.org](https://developers.uniswap.org/docs/protocols/v3/deployments/v3-ethereum-deployments); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| USDC | Stablecoin | present | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | [developers.circle.com](https://developers.circle.com/stablecoins/usdc-contract-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| USDT | Stablecoin | present | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | [tether.to](https://tether.to/en/supported-protocols/); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| DAI | Stablecoin | present | `0x6B175474E89094C44Da98b954EedeAC495271d0F` | [chainlog.sky.money](https://chainlog.sky.money/api/mainnet/active.json); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| USDS | Stablecoin | present | `0xdC035D45d973E3EC169d2276DDab16f1e407384F` | [chainlog.sky.money](https://chainlog.sky.money/api/mainnet/active.json); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| USDe | Stablecoin | present | `0x4c9EDD5852cd905f086C759E8383e09bff1E68B3` | [docs.ethena.fi](https://docs.ethena.fi/solution-design/key-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| PYUSD | Stablecoin | present | `0x6c3ea9036406852006290770BEdFcAbA0e23A0e8` | [docs.paxos.com](https://docs.paxos.com/guides/stablecoin/pyusd/mainnet); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| RLUSD | Stablecoin | present | `0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD` | [docs.ripple.com](https://docs.ripple.com/products/stablecoin/overview/token-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| USDG | Stablecoin | present | `0xe343167631d89B6Ffc58B88d6b7fB0228795491D` | [docs.paxos.com](https://docs.paxos.com/guides/stablecoin/usdg/mainnet); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| FDUSD | Stablecoin | present | `0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409` | [firstdigitallabs.com](https://www.firstdigitallabs.com/fdusd); also [web.archive.org](https://web.archive.org/web/20260417165942/https://www.firstdigitallabs.com/fdusd) | 2026-10-28 | +| USD1 | Stablecoin | present | `0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d` | [docs.worldlibertyfinancial.com](https://docs.worldlibertyfinancial.com/usd1-token/contract-addresses) | 2026-10-28 | +| EURC | Stablecoin | present | `0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c` | [developers.circle.com](https://developers.circle.com/stablecoins/eurc-contract-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| WBTC | BTC | present | `0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599` | [wbtc.network](https://wbtc.network/transparency); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| cbBTC | BTC | present | `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` | [coinbase/agentkit](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| tBTC | BTC | present | `0x18084fbA666a33d37592fA2633fD49a74DD93a88` | [docs.threshold.network](https://docs.threshold.network/contract-addresses/tbtc); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| LBTC | BTC | present | `0x8236a87084f8B84306f72007F36F2618A5634494` | [docs.lombard.finance](https://docs.lombard.finance/learn/transparency/smart-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| stETH | Liquid staking | present; transfer handling: rebasing | `0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84` | [docs.lido.fi](https://docs.lido.fi/deployed-contracts/) | 2026-10-28 | +| wstETH | Liquid staking | present | `0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0` | [docs.lido.fi](https://docs.lido.fi/deployed-contracts/); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| rETH | Liquid staking | present | `0xae78736Cd615f374D3085123A210448E74Fc6393` | [rocket-pool/docs.rocketpool.net](https://github.com/rocket-pool/docs.rocketpool.net/blob/main/docs/en/protocol/contracts-integrations.md); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| cbETH | Liquid staking | present | `0xBe9895146f7AF43049ca1c1AE358B0541Ea49704` | [coinbase/agentkit](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| weETH | Liquid staking | present | `0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee` | [etherfi.gitbook.io](https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| UNI | DEX token | present | `0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| 1INCH | Aave Core reserve | present | `0x111111111117dC0aa78b770fA6A738034120C302` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| AAVE | Aave Core reserve | present | `0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| BAL | Aave Core reserve | present | `0xba100000625a3754423978a60c9317c58a424e3D` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| BTC.b | Aave Core reserve | present | `0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072` | [docs.lombard.finance](https://docs.lombard.finance/learn/transparency/smart-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| CRV | Aave Core reserve | present | `0xD533a949740bb3306d119CC777fa900bA034cd52` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| crvUSD | Aave Core reserve | present | `0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E` | [docs.curve.finance](https://docs.curve.finance/developer/crvusd/); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| eBTC | Aave Core reserve | present | `0x657e8C867D8B37dCC18fA4Caead9C45EB088C642` | [etherfi.gitbook.io](https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| ENS | Aave Core reserve | present | `0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| ETHx | Aave Core reserve | present | `0xA35b1B31Ce002FBF2058D22F30f95D405200A15b` | [staderlabs.gitbook.io](https://staderlabs.gitbook.io/ethereum/smart-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| eUSDe | Aave Core reserve | present | `0x90D2af7d622ca3141efA4d8f1F24d86E5974Cc8F` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| ezETH | Aave Core reserve | present | `0xbf5495Efe5DB9ce00f80364C8B423567e58d2110` | [docs.renzoprotocol.com](https://docs.renzoprotocol.com/docs/contracts/ethereum-mainnet); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| FBTC | Aave Core reserve | present | `0xC96dE26018A54D51c097160568752c4E3BD6C364` | [docs.fbtc.com](https://docs.fbtc.com/developers/smart-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| FRAX | Aave Core reserve | present | `0x853d955aCEf822Db058eb8505911ED77F175b99e` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| FXS | Aave Core reserve | present | `0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| GHO | Aave Core reserve | present | `0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/GhoEthereum.sol), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| KNC | Aave Core reserve | present | `0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| LDO | Aave Core reserve | present | `0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| LINK | Aave Core reserve | present | `0x514910771AF9Ca656af840dff83E8264EcF986CA` | [docs.chain.link](https://docs.chain.link/resources/link-token-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| LUSD | Aave Core reserve | present | `0x5f98805A4E8be255a32880FDeC7F6728C6568bA0` | [docs.liquity.org](https://docs.liquity.org/liquity-v1/documentation/resources); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| MKR | Aave Core reserve | present; legacy | `0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2` | [chainlog.sky.money](https://chainlog.sky.money/api/mainnet/active.json); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| mUSD | Aave Core reserve | present | `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| osETH | Aave Core reserve | present | `0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38` | [docs.stakewise.io](https://docs.stakewise.io/contracts/networks/Mainnet); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| RPL | Aave Core reserve | present | `0xD33526068D116cE69F19A9ee46F0bd304F21A51f` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| rsETH | Aave Core reserve | present | `0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7` | [Kelp-DAO/LRT-rsETH](https://github.com/Kelp-DAO/LRT-rsETH/blob/main/README.md); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| sDAI | Aave Core reserve | present | `0x83F20F44975D03b1b09e64809B757c47f942BEeA` | [docs.spark.finance](https://docs.spark.finance/dev/savings/sdai-token); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| SNX | Aave Core reserve | present | `0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| STG | Aave Core reserve | present | `0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| sUSDe | Aave Core reserve | present | `0x9D39A5DE30e57443BfF2A8307A4256c8797A3497` | [docs.ethena.fi](https://docs.ethena.fi/solution-design/key-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| syrupUSDT | Aave Core reserve | present | `0x356B8d89c1e1239Cbbb9dE4815c39A1474d5BA7D` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| tETH | Aave Core reserve | present | `0xD11c452fc99cF405034ee446803b6F6c1F6d5ED8` | [docs.treehouse.finance](https://docs.treehouse.finance/protocol/tasset/key-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| USDtb | Aave Core reserve | present | `0xC139190F447e929f090Edeb554D95AbB8b18aC1C` | [docs.usdtb.money](https://docs.usdtb.money/key-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | +| XAUt | Aave Core reserve | present | `0x68749665FF8D2d112Fa859AA293F07A622782F38` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol) | 2026-10-28 | + +### Other verified rows + +| Asset | Status | Address | Sources | Verified | +|---|---|---|---|---| +| cmETH | present | `0xE6829d9a7eE3040e1276Fa75293Bde931859e8fA` | [docs.mantle.xyz](https://docs.mantle.xyz/meth/components/smart-contracts/restaking-cmeth) | 2026-10-28 | +| eETH | present; transfer handling: rebasing | `0x35fA164735182de50811E8e2E824cFb9B6118ac2` | [etherfi.gitbook.io](https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts) | 2026-10-28 | +| frxETH | present | `0x5E8422345238F34275888049021821E8E08CAa1f` | [docs.frax.finance](https://docs.frax.finance/frax-ether/frxeth-and-sfrxeth-token-addresses) | 2026-10-28 | +| mETH | present | `0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa` | [docs.mantle.xyz](https://docs.mantle.xyz/meth/components/smart-contracts/staking-meth) | 2026-10-28 | +| sfrxETH | present | `0xac3E018457B222d93114458476f3E3416Abbe38F` | [docs.frax.finance](https://docs.frax.finance/frax-ether/frxeth-and-sfrxeth-token-addresses) | 2026-10-28 | +| SKY | present | `0x56072C95FAA701256059aa122697B133aDEd9279` | [chainlog.sky.money](https://chainlog.sky.money/api/mainnet/active.json) | 2026-10-28 | +| SolvBTC | present | `0x7A56E1C57C7475CCf742a1832B028F0456652F97` | [solv-finance/SolvBTC](https://github.com/solv-finance/SolvBTC/blob/main/deploy/SolvBTC/10099_export_SolvBTCInfos.js) | 2026-10-28 | +| sUSDS | present | `0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD` | [chainlog.sky.money](https://chainlog.sky.money/api/mainnet/active.json) | 2026-10-28 | +| xSolvBTC | present | `0xd9D920AA40f578ab794426F5C90F6C731D159DEf` | [solv-finance/SolvBTC](https://github.com/solv-finance/SolvBTC/blob/main/deploy/xSolvBTCPool/1099_export_xSolvBTCPoolInfos.js); also [docs.chain.link](https://docs.chain.link/ccip/directory/mainnet/token/xSolvBTC) | 2026-10-28 | + +### Provenance pending (67) + +Listed before provenance tracking. `yarn verify:tokens` reads their code, decimals and symbol on-chain; no canonical source has been recorded yet. + +AIOZ, APE, ARB, ATH, AXS, BGB, BNB, Bonk, BTT, BUIDL, Cake, cgETH.hashkey, clBTC, COMP, CRO, DEXE, DYDX, EIGEN, ENA, ETHFI, FET, FLOKI, FTN, GALA, GRT, GT, IMX, INJ, JASMY, LEO, LsETH, MANA, MNT, NEAR, NEXO, NFT, OKB, ONDO, OUSG, PAXG, PENDLE, PEPE, POL, pumpBTC, QNT, RNDR, RSR, SAND, SHIB, SPX, STRK, SYRUP, syrupUSDC, TEL, TKX, TONCOIN, TUSD, USD0, USDD, USDX, USDY, USYC, VIRTUAL, WBT, WLD, XCN, ZK + +Transfer handling among them: + +- BUIDL: permissionedRecipient +- OUSG: permissionedRecipient +- USYC: permissionedRecipient + +## Base (chain 8453) + +### Baseline + +| Asset | Category | Status | Address | Sources | Verified | +|---|---|---|---|---|---| +| WETH | Native wrapper | present | `0x4200000000000000000000000000000000000006` | [docs.base.org](https://docs.base.org/base-chain/network-information/base-contracts), [specs.optimism.io](https://specs.optimism.io/protocol/predeploys.html); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| USDC | Stablecoin | present | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | [developers.circle.com](https://developers.circle.com/stablecoins/usdc-contract-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| EURC | Stablecoin | present | `0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42` | [developers.circle.com](https://developers.circle.com/stablecoins/eurc-contract-addresses); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| USDS | Stablecoin | present; bridged (Sky Base token bridge) | `0x820C137fa70C8691f0e44Dc420a5e53c168921Dc` | [sky-ecosystem/spells-mainnet](https://github.com/sky-ecosystem/spells-mainnet/blob/master/src/test/addresses_base.sol) | 2026-10-28 | +| USDe | Stablecoin | present | `0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34` | [docs.ethena.fi](https://docs.ethena.fi/solution-design/key-addresses) | 2026-10-28 | +| RLUSD | Stablecoin | present | `0x8d58C0C60B8D6b88Fa98B291a646dB34d0F98258` | [docs.ripple.com](https://docs.ripple.com/products/stablecoin/overview/token-addresses) | 2026-10-28 | +| WBTC | BTC | present | `0x1ceA84203673764244E05693e42E6Ace62bE9BA5` | [wbtc.network](https://wbtc.network/transparency), [bitgo.com](https://www.bitgo.com/resources/blog/launching-wrapped-bitcoin-wbtc-on-base) | 2026-10-28 | +| cbBTC | BTC | present | `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` | [coinbase/agentkit](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| tBTC | BTC | present; bridged (Wormhole (Threshold BaseWormholeGateway)) | `0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b` | [docs.threshold.network](https://docs.threshold.network/contract-addresses/tbtc); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| LBTC | BTC | present | `0xecAc9C5F704e954931349Da37F60E39f515c11c1` | [docs.lombard.finance](https://docs.lombard.finance/learn/transparency/smart-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| wstETH | Liquid staking | present; bridged (Lido token bridge) | `0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452` | [docs.lido.fi](https://docs.lido.fi/deployed-contracts/); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| rETH | Liquid staking | present; bridged (Base standard bridge) | `0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c` | [rocket-pool/docs.rocketpool.net](https://github.com/rocket-pool/docs.rocketpool.net/blob/main/docs/en/protocol/contracts-integrations.md) | 2026-10-28 | +| cbETH | Liquid staking | present; bridged (Base standard bridge) | `0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22` | [coinbase/agentkit](https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| weETH | Liquid staking | present; bridged (LayerZero OFT) | `0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A` | [etherfi.gitbook.io](https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| AERO | DEX token | present | `0x940181a94A35A4569E4529A3CDfB74e38FD98631` | [aerodrome-finance/contracts](https://github.com/aerodrome-finance/contracts/blob/main/README.md) | 2026-10-28 | +| AAVE | Aave Core reserve | present; bridged (Base standard bridge) | `0x63706e401c06ac8513145b7687A14804d17f814b` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| ezETH | Aave Core reserve | present; bridged (Renzo xERC20 (Connext)) | `0x2416092f143378750bb29b79eD961ab195CcEea5` | [docs.renzoprotocol.com](https://docs.renzoprotocol.com/docs/contracts/layer-2s/base); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| GHO | Aave Core reserve | present | `0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/GhoBase.sol), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| syrupUSDC | Aave Core reserve | present | `0x660975730059246A68521a3e2FBD4740173100f5` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| USDbC | Aave Core reserve | present; bridged (Base standard bridge); legacy | `0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | +| wrsETH | Aave Core reserve | present; bridged (Kelp rsETH wrapper) | `0xEDfa23602D0EC14714057867A78d01e94176BEA0` | [Kelp-DAO/LRT-rsETH](https://github.com/Kelp-DAO/LRT-rsETH/blob/main/README.md); also [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol) | 2026-10-28 | + +### Other verified rows + +| Asset | Status | Address | Sources | Verified | +|---|---|---|---|---| +| DAI | present; bridged (Base standard bridge) | `0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb` | [ethereum-optimism/ethereum-optimism.github.io](https://github.com/ethereum-optimism/ethereum-optimism.github.io) (secondary only) | 2026-10-28 | +| LINK | present | `0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196` | [docs.chain.link](https://docs.chain.link/resources/link-token-contracts) | 2026-10-28 | +| sUSDe | present | `0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2` | [docs.ethena.fi](https://docs.ethena.fi/solution-design/key-addresses) | 2026-10-28 | +| sUSDS | present; bridged (Sky Base token bridge) | `0x5875eEE11Cf8398102FdAd704C9E96607675467a` | [sky-ecosystem/spells-mainnet](https://github.com/sky-ecosystem/spells-mainnet/blob/master/src/test/addresses_base.sol) | 2026-10-28 | +| USDT | present; bridged (Base standard bridge) | `0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2` | [ethereum-optimism/ethereum-optimism.github.io](https://github.com/ethereum-optimism/ethereum-optimism.github.io) (secondary only) | 2026-10-28 | + +### Provenance pending (11) + +Listed before provenance tracking. `yarn verify:tokens` reads their code, decimals and symbol on-chain; no canonical source has been recorded yet. + +axlUSDC, BAL, BRETT, DEGEN, MORPHO, RSR, TOSHI, UNI, VIRTUAL, WELL, YFI + +## BNB Chain (chain 56) + +### Baseline + +| Asset | Category | Status | Address | Sources | Verified | +|---|---|---|---|---|---| +| WBNB | Native wrapper | present | `0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c` | [bnb-chain/bnb-chain.github.io](https://github.com/bnb-chain/bnb-chain.github.io/blob/master/docs/bnb-smart-chain/benchmark/design-reference.md), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json) | 2026-10-28 | +| USDT | Stablecoin | present; bridged (Binance-Peg) | `0x55d398326f99059fF775485246999027B3197955` | [bnb-chain/bcfusion](https://github.com/bnb-chain/bcfusion/blob/main/README.md); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | +| USDC | Stablecoin | present; bridged (Binance-Peg) | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | [bnb-chain/bcfusion](https://github.com/bnb-chain/bcfusion/blob/main/README.md); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | +| FDUSD | Stablecoin | present | `0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409` | [firstdigitallabs.com](https://www.firstdigitallabs.com/fdusd); also [web.archive.org](https://web.archive.org/web/20260417165942/https://www.firstdigitallabs.com/fdusd), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | +| USD1 | Stablecoin | present | `0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d` | [docs.worldlibertyfinancial.com](https://docs.worldlibertyfinancial.com/usd1-token/contract-addresses); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json) | 2026-10-28 | +| USDe | Stablecoin | present | `0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34` | [docs.ethena.fi](https://docs.ethena.fi/solution-design/key-addresses) | 2026-10-28 | +| BTCB | BTC | present; bridged (Binance-Peg) | `0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c` | [bnb-chain/bcfusion](https://github.com/bnb-chain/bcfusion/blob/main/README.md); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | +| LBTC | BTC | present | `0xecAc9C5F704e954931349Da37F60E39f515c11c1` | [docs.lombard.finance](https://docs.lombard.finance/learn/transparency/smart-contracts) | 2026-10-28 | +| wstETH | Liquid staking | present; bridged (Wormhole NTT) | `0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C` | [docs.lido.fi](https://docs.lido.fi/deployed-contracts/); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | +| slisBNB | Liquid staking | present | `0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B` | [docs.bsc.lista.org](https://docs.bsc.lista.org/for-developer/liquid-staking-slisbnb/smart-contract); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json) | 2026-10-28 | +| Cake | DEX token | present | `0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82` | [docs.pancakeswap.finance](https://docs.pancakeswap.finance/protocol/cake-tokenomics); also [bnb-chain/bcfusion](https://github.com/bnb-chain/bcfusion/blob/main/README.md), [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | +| ETH | Aave Core reserve | present; bridged (Binance-Peg) | `0x2170Ed0880ac9A755fd29B2688956BD959F933F8` | [bnb-chain/bcfusion](https://github.com/bnb-chain/bcfusion/blob/main/README.md); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json), [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol) | 2026-10-28 | + +### Other verified rows + +| Asset | Status | Address | Sources | Verified | +|---|---|---|---|---| +| BNBx | present; legacy | `0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275` | [stader.gitbook.io](https://stader.gitbook.io/stader/bnb-chain-discontinued); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json) | 2026-10-28 | +| LINK | present | `0x404460C6A5EdE2D891e8297795264fDe62ADBB75` | [docs.chain.link](https://docs.chain.link/resources/link-token-contracts) | 2026-10-28 | +| SolvBTC | present | `0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7` | [solv-finance/SolvBTC](https://github.com/solv-finance/SolvBTC/blob/main/deploy/SolvBTC/10099_export_SolvBTCInfos.js); also [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json) | 2026-10-28 | +| sUSDe | present | `0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2` | [docs.ethena.fi](https://docs.ethena.fi/solution-design/key-addresses) | 2026-10-28 | +| XVS | present | `0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63` | [docs-v4.venus.io](https://docs-v4.venus.io/deployed-contracts/xvs-omnichain); also [bnb-chain/bcfusion](https://github.com/bnb-chain/bcfusion/blob/main/README.md), [pancakeswap/token-list](https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json) | 2026-10-28 | + +## Sepolia (chain 11155111) + +### Baseline + +| Asset | Category | Status | Address | Sources | Verified | +|---|---|---|---|---|---| +| WETH | Native wrapper | present; test token (WETH9 used by Uniswap) | `0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14` | [developers.uniswap.org](https://developers.uniswap.org/docs/protocols/v3/deployments/v3-ethereum-deployments) | 2026-10-28 | +| USDC | Test token | present; test token (Circle) | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` | [developers.circle.com](https://developers.circle.com/stablecoins/usdc-contract-addresses) | 2026-10-28 | +| EURC | Test token | present; test token (Circle) | `0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4` | [developers.circle.com](https://developers.circle.com/stablecoins/eurc-contract-addresses) | 2026-10-28 | +| LINK | Test token | present; test token (Chainlink) | `0x779877A7B0D9E8603169DdbD7836e478b4624789` | [docs.chain.link](https://docs.chain.link/resources/link-token-contracts) | 2026-10-28 | + +### Other verified rows + +| Asset | Status | Address | Sources | Verified | +|---|---|---|---|---| +| UNI | present; test token (Uniswap) | `0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984` | [eth-sepolia.blockscout.com](https://eth-sepolia.blockscout.com/address/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984), [eth.blockscout.com](https://eth.blockscout.com/address/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984) (secondary only) | 2026-10-28 | +| USDT | present; test token (Aave V3 testnet faucet (not Tether)) | `0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0` | [bgd-labs/aave-address-book](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Sepolia.sol) | 2026-10-28 | + +### Provenance pending (1) + +Listed before provenance tracking. `yarn verify:tokens` reads their code, decimals and symbol on-chain; no canonical source has been recorded yet. + +DAI + +## Base Sepolia (chain 84532) + +### Baseline + +| Asset | Category | Status | Address | Sources | Verified | +|---|---|---|---|---|---| +| WETH | Native wrapper | present; test token (OP Stack WETH9 predeploy) | `0x4200000000000000000000000000000000000006` | [docs.base.org](https://docs.base.org/base-chain/network-information/base-contracts) | 2026-10-28 | +| USDC | Test token | present; test token (Circle) | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | [developers.circle.com](https://developers.circle.com/stablecoins/usdc-contract-addresses) | 2026-10-28 | +| EURC | Test token | present; test token (Circle) | `0x808456652fdb597867f38412077A9182bf77359F` | [developers.circle.com](https://developers.circle.com/stablecoins/eurc-contract-addresses) | 2026-10-28 | +| LINK | Test token | present; test token (Chainlink) | `0xE4aB69C077896252FAFBD49EFD26B5D171A32410` | [docs.chain.link](https://docs.chain.link/resources/link-token-contracts) | 2026-10-28 | + +### Provenance pending (1) + +Listed before provenance tracking. `yarn verify:tokens` reads their code, decimals and symbol on-chain; no canonical source has been recorded yet. + +USDT diff --git a/package.json b/package.json index a7a4c40..5feec84 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,8 @@ "port:studio": "tsx scripts/port-from-studio.ts", "port:avs": "tsx scripts/port-from-avs.ts", "generate:aave-reserves": "tsx scripts/generate-aave-reserves.ts", + "verify:tokens": "tsx scripts/verify-tokens.ts", + "report:tokens": "tsx scripts/token-coverage-report.ts", "clean": "rm -rf node_modules dist tsconfig.tsbuildinfo", "clean:dist": "rm -rf dist tsconfig.tsbuildinfo", "test": "vitest", diff --git a/scripts/token-catalog/coverage.ts b/scripts/token-catalog/coverage.ts new file mode 100644 index 0000000..539608d --- /dev/null +++ b/scripts/token-catalog/coverage.ts @@ -0,0 +1,181 @@ +/** + * The token catalog's coverage target: which assets each catalog chain + * must list, which known gaps remain, and which pre-audit rows still lack + * provenance. `tests/token-coverage.test.ts` enforces it and + * `scripts/token-coverage-report.ts` renders it to docs/TOKEN_COVERAGE.md. + * + * The selection rules are in docs/TOKEN_CATALOG.md ("Coverage target"). + * In short, per mainnet: + * 1. the canonical wrapper of the native coin (`Protocols.wrapped.weth`) + * 2. major fiat stablecoins, on chains where the issuer publishes an address + * 3. major BTC representations, on chains where the issuer publishes an address + * 4. major liquid-staking assets, on chains where the issuer publishes an address + * 5. every Aave V3 Core reserve on the chain except expiring Pendle PTs, + * plus the chain's main DEX governance token + * Testnets list verified test deployments only (rule 6). + * + * The list is explicit on purpose. "All common tokens" is not testable; + * this is. + */ + +import { Chains } from "../../src/chains"; +import { Protocols } from "../../src/protocols"; + +export const COVERAGE_CATEGORY = Object.freeze({ + nativeWrapper: "nativeWrapper", + stablecoin: "stablecoin", + bitcoin: "bitcoin", + liquidStaking: "liquidStaking", + defi: "defi", + aaveReserve: "aaveReserve", + testToken: "testToken", +} as const); + +export type CoverageCategory = (typeof COVERAGE_CATEGORY)[keyof typeof COVERAGE_CATEGORY]; + +export interface BaselineAsset { + readonly symbol: string; + readonly category: CoverageCategory; +} + +/** Catalog chains, in report order. Mainnets first. */ +export const CATALOG_CHAINS: readonly number[] = [ + Chains.EthereumMainnet, + Chains.BaseMainnet, + Chains.BnbMainnet, + Chains.Sepolia, + Chains.BaseSepolia, +]; + +export const CHAIN_LABEL: Readonly> = Object.freeze({ + [Chains.EthereumMainnet]: "Ethereum", + [Chains.BaseMainnet]: "Base", + [Chains.BnbMainnet]: "BNB Chain", + [Chains.Sepolia]: "Sepolia", + [Chains.BaseSepolia]: "Base Sepolia", +}); + +const { nativeWrapper, stablecoin, bitcoin, liquidStaking, defi, testToken } = COVERAGE_CATEGORY; + +/** Rules 1–4 and 6, plus rule 5's DEX token. Rule 5's Aave reserves are derived below. */ +export const EXPLICIT_BASELINE: Readonly> = Object.freeze({ + [Chains.EthereumMainnet]: [ + { symbol: "WETH", category: nativeWrapper }, + { symbol: "USDC", category: stablecoin }, + { symbol: "USDT", category: stablecoin }, + { symbol: "DAI", category: stablecoin }, + { symbol: "USDS", category: stablecoin }, + { symbol: "USDe", category: stablecoin }, + { symbol: "PYUSD", category: stablecoin }, + { symbol: "RLUSD", category: stablecoin }, + { symbol: "USDG", category: stablecoin }, + { symbol: "FDUSD", category: stablecoin }, + { symbol: "USD1", category: stablecoin }, + { symbol: "EURC", category: stablecoin }, + { symbol: "WBTC", category: bitcoin }, + { symbol: "cbBTC", category: bitcoin }, + { symbol: "tBTC", category: bitcoin }, + { symbol: "LBTC", category: bitcoin }, + { symbol: "stETH", category: liquidStaking }, + { symbol: "wstETH", category: liquidStaking }, + { symbol: "rETH", category: liquidStaking }, + { symbol: "cbETH", category: liquidStaking }, + { symbol: "weETH", category: liquidStaking }, + { symbol: "UNI", category: defi }, + ], + [Chains.BaseMainnet]: [ + { symbol: "WETH", category: nativeWrapper }, + { symbol: "USDC", category: stablecoin }, + { symbol: "EURC", category: stablecoin }, + { symbol: "USDS", category: stablecoin }, + { symbol: "USDe", category: stablecoin }, + { symbol: "RLUSD", category: stablecoin }, + { symbol: "WBTC", category: bitcoin }, + { symbol: "cbBTC", category: bitcoin }, + { symbol: "tBTC", category: bitcoin }, + { symbol: "LBTC", category: bitcoin }, + { symbol: "wstETH", category: liquidStaking }, + { symbol: "rETH", category: liquidStaking }, + { symbol: "cbETH", category: liquidStaking }, + { symbol: "weETH", category: liquidStaking }, + { symbol: "AERO", category: defi }, + ], + [Chains.BnbMainnet]: [ + { symbol: "WBNB", category: nativeWrapper }, + { symbol: "USDT", category: stablecoin }, + { symbol: "USDC", category: stablecoin }, + { symbol: "FDUSD", category: stablecoin }, + { symbol: "USD1", category: stablecoin }, + { symbol: "USDe", category: stablecoin }, + { symbol: "BTCB", category: bitcoin }, + { symbol: "LBTC", category: bitcoin }, + { symbol: "wstETH", category: liquidStaking }, + { symbol: "slisBNB", category: liquidStaking }, + { symbol: "Cake", category: defi }, + ], + [Chains.Sepolia]: [ + { symbol: "WETH", category: nativeWrapper }, + { symbol: "USDC", category: testToken }, + { symbol: "EURC", category: testToken }, + { symbol: "LINK", category: testToken }, + ], + [Chains.BaseSepolia]: [ + { symbol: "WETH", category: nativeWrapper }, + { symbol: "USDC", category: testToken }, + { symbol: "EURC", category: testToken }, + { symbol: "LINK", category: testToken }, + ], +}); + +/** Pendle principal tokens expire; Aave lists them as collateral for a few months each. */ +const EXPIRING_RESERVE = /^PT-/; + +/** + * Rule 5: Aave V3 Core reserves on a mainnet, matched by address. Testnet + * reserves are Aave faucet tokens and are excluded. + */ +export function aaveCoreReserves(chainId: number): ReadonlyArray<{ readonly symbol: string; readonly underlying: string }> { + if (chainId === Chains.Sepolia || chainId === Chains.BaseSepolia) return []; + return (Protocols.aaveV3.reserves[chainId] ?? []).filter((reserve) => !EXPIRING_RESERVE.test(reserve.symbol)); +} + +export interface KnownGap { + readonly chainId: number; + readonly symbol: string; + readonly reason: string; +} + +/** + * Baseline assets the catalog does not list yet. The test fails when a + * listed gap is filled, so this list stays current. + */ +export const KNOWN_GAPS: readonly KnownGap[] = []; + +/** + * Catalog rows that predate provenance tracking and have no provenance row + * yet. On-chain reads (`yarn verify:tokens`) cover them; canonical sources + * do not. New rows may not be added here: the test fails if a chain's list + * grows past `PROVENANCE_PENDING_AT_AUDIT`, and when a listed row gains + * provenance. + */ +export const PROVENANCE_PENDING: Readonly> = Object.freeze({ + [Chains.EthereumMainnet]: [ + "AIOZ", "APE", "ARB", "ATH", "AXS", "BGB", "BNB", "Bonk", "BTT", "BUIDL", "Cake", + "cgETH.hashkey", "clBTC", "COMP", "CRO", "DEXE", "DYDX", "EIGEN", "ENA", "ETHFI", "FET", + "FLOKI", "FTN", "GALA", "GRT", "GT", "IMX", "INJ", "JASMY", "LEO", "LsETH", "MANA", "MNT", + "NEAR", "NEXO", "NFT", "OKB", "ONDO", "OUSG", "PAXG", "PENDLE", "PEPE", "POL", "pumpBTC", "QNT", + "RNDR", "RSR", "SAND", "SHIB", "SPX", "STRK", "SYRUP", "syrupUSDC", "TEL", "TKX", + "TONCOIN", "TUSD", "USD0", "USDD", "USDX", "USDY", "USYC", "VIRTUAL", "WBT", "WLD", "XCN", "ZK", + ], + [Chains.BaseMainnet]: ["axlUSDC", "BAL", "BRETT", "DEGEN", "MORPHO", "RSR", "TOSHI", "UNI", "VIRTUAL", "WELL", "YFI"], + [Chains.Sepolia]: ["DAI"], + [Chains.BaseSepolia]: ["USDT"], +}); + +/** Size of each chain's pending list at the 2026-10-28 audit. Lower it as rows gain provenance; never raise it. */ +export const PROVENANCE_PENDING_AT_AUDIT: Readonly> = Object.freeze({ + [Chains.EthereumMainnet]: 67, + [Chains.BaseMainnet]: 11, + [Chains.Sepolia]: 1, + [Chains.BaseSepolia]: 1, +}); diff --git a/scripts/token-catalog/provenance.ts b/scripts/token-catalog/provenance.ts new file mode 100644 index 0000000..e59aae2 --- /dev/null +++ b/scripts/token-catalog/provenance.ts @@ -0,0 +1,65 @@ +/** + * Token catalog provenance: where each catalog row's address comes from. + * + * One JSON file per catalog chain under `src/tokens/provenance/`, next to + * the `src/tokens/data/` file it describes. No module under `src/` + * imports these files, so the build does not ship them; the coverage + * test and the report script read them. + * + * A provenance row pins one catalog row's address to the sources that + * vouch for it. `tests/token-coverage.test.ts` fails when the two + * disagree, so moving an address means updating its sources too. + * + * See docs/TOKEN_CATALOG.md for the inclusion policy. + */ + +import ethereum from "../../src/tokens/provenance/ethereum.json" with { type: "json" }; +import base from "../../src/tokens/provenance/base.json" with { type: "json" }; +import bnbMainnet from "../../src/tokens/provenance/bnb-mainnet.json" with { type: "json" }; +import sepolia from "../../src/tokens/provenance/sepolia.json" with { type: "json" }; +import baseSepolia from "../../src/tokens/provenance/base-sepolia.json" with { type: "json" }; +import { Chains } from "../../src/chains"; + +/** How the token came to exist on this chain. */ +export const TOKEN_ISSUANCE = Object.freeze({ + /** Minted on this chain by the asset's issuer or protocol (including its canonical wrapper of the native coin). */ + native: "native", + /** A representation of an asset native elsewhere, minted by a bridge or a third party. `bridge` names it. */ + bridged: "bridged", + /** A testnet deployment. `issuer` says who deployed it; it may not be the mainnet issuer. */ + test: "test", +} as const); + +export type TokenIssuance = (typeof TOKEN_ISSUANCE)[keyof typeof TOKEN_ISSUANCE]; + +export interface TokenProvenanceRow { + readonly symbol: string; + readonly address: string; + readonly issuance: TokenIssuance; + /** The organisation that publishes the address (issuer, protocol, or bridge operator). */ + readonly issuer: string; + /** Present when `issuance` is `bridged`: the bridge or operator (e.g. "Binance-Peg", "Base standard bridge"). */ + readonly bridge?: string; + /** Superseded by a newer deployment or no longer issued. Holdings still exist, so the row stays. */ + readonly legacy?: boolean; + /** Issuer or protocol pages (or official repositories) that list this exact address on this chain. */ + readonly sources: readonly string[]; + /** Secondary evidence: maintained token lists, explorer pages, deployer matches. */ + readonly references?: readonly string[]; + /** ISO date (YYYY-MM-DD) the sources and the on-chain read were last checked. */ + readonly verifiedAt: string; + readonly notes?: string; +} + +/** Catalog chains with a provenance file, in report order. */ +export const PROVENANCE_BY_CHAIN: ReadonlyArray = [ + [Chains.EthereumMainnet, ethereum as readonly TokenProvenanceRow[]], + [Chains.BaseMainnet, base as readonly TokenProvenanceRow[]], + [Chains.BnbMainnet, bnbMainnet as readonly TokenProvenanceRow[]], + [Chains.Sepolia, sepolia as readonly TokenProvenanceRow[]], + [Chains.BaseSepolia, baseSepolia as readonly TokenProvenanceRow[]], +]; + +export function provenanceFor(chainId: number): readonly TokenProvenanceRow[] { + return PROVENANCE_BY_CHAIN.find(([id]) => id === chainId)?.[1] ?? []; +} diff --git a/scripts/token-coverage-report.ts b/scripts/token-coverage-report.ts new file mode 100644 index 0000000..d622fbb --- /dev/null +++ b/scripts/token-coverage-report.ts @@ -0,0 +1,183 @@ +/** + * Render docs/TOKEN_COVERAGE.md, the per-chain coverage matrix, from the + * catalog data, the provenance files and the coverage target. + * + * yarn report:tokens + * + * `tests/token-coverage.test.ts` fails when the committed file differs + * from what this renders, so run it after any token data change. + */ + +import { writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { Tokens, type TokenChainEntry } from "../src"; +import { + CATALOG_CHAINS, + CHAIN_LABEL, + COVERAGE_CATEGORY, + EXPLICIT_BASELINE, + KNOWN_GAPS, + PROVENANCE_PENDING, + aaveCoreReserves, + type CoverageCategory, +} from "./token-catalog/coverage"; +import { TOKEN_ISSUANCE, provenanceFor, type TokenProvenanceRow } from "./token-catalog/provenance"; + +const OUTPUT = resolve(dirname(fileURLToPath(import.meta.url)), "../docs/TOKEN_COVERAGE.md"); + +const CATEGORY_LABEL: Readonly> = { + [COVERAGE_CATEGORY.nativeWrapper]: "Native wrapper", + [COVERAGE_CATEGORY.stablecoin]: "Stablecoin", + [COVERAGE_CATEGORY.bitcoin]: "BTC", + [COVERAGE_CATEGORY.liquidStaking]: "Liquid staking", + [COVERAGE_CATEGORY.defi]: "DEX token", + [COVERAGE_CATEGORY.aaveReserve]: "Aave Core reserve", + [COVERAGE_CATEGORY.testToken]: "Test token", +}; + +interface ChainRow { + readonly symbol: string; + readonly entry: TokenChainEntry; +} + +function catalogRows(chainId: number): ChainRow[] { + const rows: ChainRow[] = []; + for (const [symbol, byChain] of Object.entries(Tokens)) { + const entry = (byChain as Record)[chainId]; + if (entry) rows.push({ symbol, entry }); + } + return rows.sort((left, right) => left.symbol.localeCompare(right.symbol, "en", { sensitivity: "base" })); +} + +/** Label a source link by host, or by `owner/repo` for GitHub, where the host alone says nothing. */ +function link(url: string): string { + const parsed = new URL(url); + const host = parsed.hostname.replace(/^www\./, ""); + const label = host === "github.com" ? parsed.pathname.split("/").slice(1, 3).join("/") : host; + return `[${label}](${url})`; +} + +function status(entry: TokenChainEntry | undefined, provenance: TokenProvenanceRow | undefined, gap?: string): string { + if (!entry) return `**missing** — ${gap ?? "not listed"}`; + const flags: string[] = []; + if (!provenance) flags.push("unverified provenance"); + else { + if (provenance.issuance === TOKEN_ISSUANCE.bridged) flags.push(`bridged (${provenance.bridge})`); + if (provenance.issuance === TOKEN_ISSUANCE.test) flags.push(`test token (${provenance.issuer})`); + if (provenance.legacy) flags.push("legacy"); + } + if (entry.transferConstraints?.length) flags.push(`transfer handling: ${entry.transferConstraints.join(", ")}`); + return flags.length ? `present; ${flags.join("; ")}` : "present"; +} + +function sourceCell(provenance: TokenProvenanceRow | undefined): string { + if (!provenance) return "—"; + const primary = provenance.sources.map(link).join(", "); + const secondary = (provenance.references ?? []).map(link).join(", "); + if (primary && secondary) return `${primary}; also ${secondary}`; + return primary || `${secondary} (secondary only)`; +} + +function renderChain(chainId: number): string[] { + const label = CHAIN_LABEL[chainId]; + const rows = catalogRows(chainId); + const provenance = new Map(provenanceFor(chainId).map((row) => [row.symbol, row])); + const gaps = new Map(KNOWN_GAPS.filter((gap) => gap.chainId === chainId).map((gap) => [gap.symbol, gap.reason])); + + // Baseline: explicit assets, then Aave Core reserves not already named. + const baseline: Array<{ symbol: string; category: CoverageCategory; row?: ChainRow }> = []; + const covered = new Set(); + for (const asset of EXPLICIT_BASELINE[chainId] ?? []) { + const row = rows.find((candidate) => candidate.symbol === asset.symbol); + baseline.push({ ...asset, row }); + if (row) covered.add(row.entry.address.toLowerCase()); + covered.add(asset.symbol); + } + const reserves = [...aaveCoreReserves(chainId)].sort((left, right) => + left.symbol.localeCompare(right.symbol, "en", { sensitivity: "base" }), + ); + for (const reserve of reserves) { + if (covered.has(reserve.underlying.toLowerCase()) || covered.has(reserve.symbol)) continue; + const row = rows.find((candidate) => candidate.entry.address.toLowerCase() === reserve.underlying.toLowerCase()); + baseline.push({ symbol: row?.symbol ?? reserve.symbol, category: COVERAGE_CATEGORY.aaveReserve, row }); + covered.add(reserve.underlying.toLowerCase()); + } + + const lines = [`## ${label} (chain ${chainId})`, ""]; + lines.push("### Baseline", ""); + lines.push("| Asset | Category | Status | Address | Sources | Verified |", "|---|---|---|---|---|---|"); + for (const asset of baseline) { + const record = asset.row ? provenance.get(asset.row.symbol) : undefined; + lines.push( + `| ${asset.symbol} | ${CATEGORY_LABEL[asset.category]} | ${status(asset.row?.entry, record, gaps.get(asset.symbol))} | ${ + asset.row ? `\`${asset.row.entry.address}\`` : "—" + } | ${sourceCell(record)} | ${record?.verifiedAt ?? "—"} |`, + ); + } + + const baselineSymbols = new Set(baseline.map((asset) => asset.row?.symbol).filter(Boolean)); + const others = rows.filter((row) => !baselineSymbols.has(row.symbol)); + const verified = others.filter((row) => provenance.has(row.symbol)); + const pending = others.filter((row) => !provenance.has(row.symbol)); + if (verified.length) { + lines.push("", "### Other verified rows", ""); + lines.push("| Asset | Status | Address | Sources | Verified |", "|---|---|---|---|---|"); + for (const row of verified) { + const record = provenance.get(row.symbol); + lines.push(`| ${row.symbol} | ${status(row.entry, record)} | \`${row.entry.address}\` | ${sourceCell(record)} | ${record?.verifiedAt} |`); + } + } + if (pending.length) { + const constrained = pending.filter((row) => row.entry.transferConstraints?.length); + lines.push("", `### Provenance pending (${pending.length})`, ""); + lines.push( + "Listed before provenance tracking. `yarn verify:tokens` reads their code, decimals and symbol on-chain; no canonical source has been recorded yet.", + "", + pending.map((row) => row.symbol).join(", "), + ); + if (constrained.length) { + lines.push("", "Transfer handling among them:", ""); + for (const row of constrained) lines.push(`- ${row.symbol}: ${row.entry.transferConstraints!.join(", ")}`); + } + } + lines.push(""); + return lines; +} + +export function renderCoverageReport(): string { + const lines = [ + "# Token catalog coverage", + "", + "", + "", + "Per-chain coverage of the shared token catalog against the target in `scripts/token-catalog/coverage.ts`. Selection rules, status definitions and the maintenance policy are in [TOKEN_CATALOG.md](./TOKEN_CATALOG.md).", + "", + "Status: **present** (listed, canonical source recorded); **bridged** or **test token** (listed variant, with who issues it); **legacy** (superseded deployment kept for existing holdings); **unverified provenance** (listed, on-chain checked, no canonical source recorded); **transfer handling** (listed with transfer constraints); **missing** (in the target, not listed yet).", + "", + "## Summary", + "", + "| Chain | Rows | Provenance recorded | Provenance pending | Known gaps | With transfer constraints |", + "|---|---|---|---|---|---|", + ]; + for (const chainId of CATALOG_CHAINS) { + const rows = catalogRows(chainId); + const recorded = new Set(provenanceFor(chainId).map((row) => row.symbol)); + lines.push( + `| ${CHAIN_LABEL[chainId]} | ${rows.length} | ${rows.filter((row) => recorded.has(row.symbol)).length} | ${ + (PROVENANCE_PENDING[chainId] ?? []).length + } | ${KNOWN_GAPS.filter((gap) => gap.chainId === chainId).length} | ${ + rows.filter((row) => row.entry.transferConstraints?.length).length + } |`, + ); + } + lines.push(""); + for (const chainId of CATALOG_CHAINS) lines.push(...renderChain(chainId)); + return lines.join("\n"); +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + writeFileSync(OUTPUT, renderCoverageReport()); + console.log(`wrote ${OUTPUT}`); +} diff --git a/scripts/verify-tokens.ts b/scripts/verify-tokens.ts new file mode 100644 index 0000000..0be2ffd --- /dev/null +++ b/scripts/verify-tokens.ts @@ -0,0 +1,272 @@ +/** + * Read-only on-chain check of the token catalog. + * + * For every row in `src/tokens/data/.json` this calls the + * contract and compares what the chain says with what the catalog + * says: + * - contract code exists at the address + * - `decimals()` equals the catalog's `decimals` + * - `symbol()` matches the catalog symbol (case-insensitive) + * + * A symbol mismatch is a warning, not an error: a few catalog keys + * deliberately differ from the on-chain string (see the per-row + * provenance notes). Missing code or a decimals mismatch is an error. + * + * This is the drift check from docs/TOKEN_CATALOG.md. It needs network + * access, so it is not part of `yarn test`; run it when adding or + * changing rows and before a release: + * + * yarn verify:tokens # every chain + * yarn verify:tokens --chain 56 # one chain + * yarn verify:tokens --json # machine-readable output + * yarn verify:tokens --rows new.json # check candidate rows before adding them + * + * `--rows` takes a JSON array of `{ chainId, symbol, address, decimals }` + * and checks those instead of the catalog files. Every row must be + * well-formed and on a catalog chain (and on `--chain`, when given): + * a row the run would skip fails it before any RPC call, so a clean + * result means every candidate was checked. + * + * RPC endpoints default to public PublicNode endpoints. Override per + * chain with `RPC_URL_` (e.g. `RPC_URL_1=https://...`). + * + * No transactions, no signatures — `eth_getCode` and `eth_call` only. + */ + +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { Contract, JsonRpcProvider, decodeBytes32String, isAddress } from "ethers"; + +import { Chains } from "../src/chains"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +interface ChainTarget { + readonly chainId: number; + readonly file: string; + readonly defaultRpc: string; +} + +const CHAIN_TARGETS: readonly ChainTarget[] = [ + { chainId: Chains.EthereumMainnet, file: "ethereum.json", defaultRpc: "https://ethereum-rpc.publicnode.com" }, + { chainId: Chains.BaseMainnet, file: "base.json", defaultRpc: "https://base-rpc.publicnode.com" }, + { chainId: Chains.BnbMainnet, file: "bnb-mainnet.json", defaultRpc: "https://bsc-rpc.publicnode.com" }, + { chainId: Chains.Sepolia, file: "sepolia.json", defaultRpc: "https://ethereum-sepolia-rpc.publicnode.com" }, + { chainId: Chains.BaseSepolia, file: "base-sepolia.json", defaultRpc: "https://base-sepolia-rpc.publicnode.com" }, +]; + +const CATALOG_CHAIN_IDS: ReadonlySet = new Set(CHAIN_TARGETS.map((target) => target.chainId)); +const MAX_UINT8 = 255; + +interface DataRow { + readonly chainId?: number; + readonly symbol: string; + readonly address: string; + readonly decimals: number; +} + +export interface CandidateRow extends DataRow { + readonly chainId: number; +} + +interface RowResult { + readonly chainId: number; + readonly symbol: string; + readonly address: string; + readonly hasCode: boolean; + readonly onchainDecimals?: number; + readonly onchainSymbol?: string; + readonly onchainName?: string; + readonly errors: string[]; + readonly warnings: string[]; +} + +// `string` and `bytes32` variants — MKR and a few older tokens return +// bytes32 from symbol()/name(). +const ERC20_ABI = [ + "function decimals() view returns (uint8)", + "function symbol() view returns (string)", + "function name() view returns (string)", +]; +const ERC20_BYTES32_ABI = ["function symbol() view returns (bytes32)", "function name() view returns (bytes32)"]; + +async function readText( + contract: Contract, + fallback: Contract, + method: "symbol" | "name", +): Promise { + try { + return String(await contract[method]()); + } catch { + try { + return decodeBytes32String(await fallback[method]()); + } catch { + return undefined; + } + } +} + +async function checkRow(provider: JsonRpcProvider, chainId: number, row: DataRow): Promise { + const errors: string[] = []; + const warnings: string[] = []; + const code = await provider.getCode(row.address); + const hasCode = code !== "0x"; + if (!hasCode) { + errors.push("no contract code at address"); + return { chainId, symbol: row.symbol, address: row.address, hasCode, errors, warnings }; + } + const contract = new Contract(row.address, ERC20_ABI, provider); + const bytes32 = new Contract(row.address, ERC20_BYTES32_ABI, provider); + + let onchainDecimals: number | undefined; + try { + onchainDecimals = Number(await contract.decimals()); + } catch { + errors.push("decimals() reverted"); + } + const onchainSymbol = await readText(contract, bytes32, "symbol"); + const onchainName = await readText(contract, bytes32, "name"); + + if (onchainDecimals !== undefined && onchainDecimals !== row.decimals) { + errors.push(`decimals: catalog ${row.decimals}, chain ${onchainDecimals}`); + } + if (onchainSymbol === undefined) { + warnings.push("symbol() unreadable"); + } else if (onchainSymbol.toLowerCase() !== row.symbol.toLowerCase()) { + warnings.push(`symbol: catalog ${row.symbol}, chain ${onchainSymbol}`); + } + return { + chainId, + symbol: row.symbol, + address: row.address, + hasCode, + onchainDecimals, + onchainSymbol, + onchainName, + errors, + warnings, + }; +} + +/** Small concurrency limiter so public endpoints don't rate-limit us. */ +async function mapLimited(items: readonly T[], limit: number, fn: (item: T) => Promise): Promise { + const out: R[] = new Array(items.length); + let next = 0; + async function worker() { + while (next < items.length) { + const index = next++; + out[index] = await fn(items[index]); + } + } + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); + return out; +} + +const FLAG = { chain: "--chain", rows: "--rows", json: "--json" } as const; +const USAGE = `usage: verify-tokens [${FLAG.chain} ] [${FLAG.rows} ] [${FLAG.json}]`; + +export interface VerifyArgs { + readonly chain?: number; + readonly json: boolean; + readonly rowsFile?: string; +} + +/** Strict: an unknown flag or a flag without its value throws, rather than falling back to the whole catalog. */ +export function parseArgs(argv: readonly string[]): VerifyArgs { + let chain: number | undefined; + let rowsFile: string | undefined; + let json = false; + for (let index = 0; index < argv.length; index++) { + const arg = argv[index]; + if (arg === "--") continue; + if (arg === FLAG.json) { + json = true; + continue; + } + if (arg === FLAG.chain || arg === FLAG.rows) { + const value = argv[++index]; + if (value === undefined || value.startsWith("--")) throw new Error(`${arg} needs a value; ${USAGE}`); + if (arg === FLAG.chain) chain = Number(value); + else rowsFile = value; + continue; + } + throw new Error(`unknown argument ${arg}; ${USAGE}`); + } + return { chain, json, rowsFile }; +} + +/** + * Validate `--rows` input before any RPC call. Collects every problem, so + * one run reports all bad rows. A row on a chain the run would not visit + * (not a catalog chain, or not `--chain`) is a problem, not a skip. + */ +export function parseCandidateRows(input: unknown, chain?: number): CandidateRow[] { + if (!Array.isArray(input)) throw new Error(`${FLAG.rows}: expected a JSON array of { chainId, symbol, address, decimals }`); + if (input.length === 0) throw new Error(`${FLAG.rows}: the file has no rows`); + const problems: string[] = []; + input.forEach((row: unknown, index) => { + if (typeof row !== "object" || row === null || Array.isArray(row)) { + problems.push(`row ${index}: not an object`); + return; + } + const { chainId, symbol, address, decimals } = row as Record; + const where = `row ${index}${typeof symbol === "string" && symbol ? ` (${symbol})` : ""}`; + if (typeof chainId !== "number" || !CATALOG_CHAIN_IDS.has(chainId)) { + problems.push(`${where}: chainId ${JSON.stringify(chainId)} is not a catalog chain (${[...CATALOG_CHAIN_IDS].join(", ")})`); + } else if (chain !== undefined && chainId !== chain) { + problems.push(`${where}: chainId ${chainId}, but ${FLAG.chain} ${chain}`); + } + if (typeof symbol !== "string" || symbol.trim() === "") problems.push(`${where}: symbol missing`); + if (typeof address !== "string" || !isAddress(address)) { + problems.push(`${where}: address ${JSON.stringify(address)} is not an address, or has a bad EIP-55 checksum`); + } + if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > MAX_UINT8) { + problems.push(`${where}: decimals ${JSON.stringify(decimals)} is not an integer from 0 to ${MAX_UINT8}`); + } + }); + if (problems.length > 0) throw new Error(`${FLAG.rows}: ${problems.length} problem(s), nothing checked:\n ${problems.join("\n ")}`); + return input as CandidateRow[]; +} + +async function main() { + const { chain, json, rowsFile } = parseArgs(process.argv.slice(2)); + const targets = CHAIN_TARGETS.filter((target) => chain === undefined || target.chainId === chain); + if (targets.length === 0) throw new Error(`no catalog chain ${chain}`); + const candidateRows = rowsFile ? parseCandidateRows(JSON.parse(readFileSync(resolve(rowsFile), "utf8")), chain) : undefined; + + const results: RowResult[] = []; + for (const target of targets) { + const rows = candidateRows + ? candidateRows.filter((row) => row.chainId === target.chainId) + : (JSON.parse(readFileSync(resolve(ROOT, "src/tokens/data", target.file), "utf8")) as DataRow[]); + if (rows.length === 0) continue; + const rpc = process.env[`RPC_URL_${target.chainId}`] ?? target.defaultRpc; + const provider = new JsonRpcProvider(rpc, target.chainId, { staticNetwork: true }); + results.push(...(await mapLimited(rows, 6, (row) => checkRow(provider, target.chainId, row)))); + provider.destroy(); + } + if (candidateRows && results.length !== candidateRows.length) { + throw new Error(`checked ${results.length} of ${candidateRows.length} candidate rows`); + } + + if (json) { + console.log(JSON.stringify(results, null, 2)); + } else { + for (const result of results) { + for (const message of result.errors) console.log(`ERROR ${result.chainId} ${result.symbol} ${result.address}: ${message}`); + for (const message of result.warnings) console.log(`warn ${result.chainId} ${result.symbol} ${result.address}: ${message}`); + } + const errorCount = results.filter((result) => result.errors.length > 0).length; + console.log(`${results.length} rows checked, ${errorCount} with errors`); + } + if (results.some((result) => result.errors.length > 0)) process.exitCode = 1; +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/src/index.ts b/src/index.ts index adc10be..9773e3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,5 +16,5 @@ export * from "./protocols"; export { Chains, type ChainId } from "./chains"; -export { Tokens, lookupToken } from "./tokens"; -export type { TokenByChain, TokenChainEntry, TokenLinks } from "./tokens"; +export { Tokens, lookupToken, TOKEN_TRANSFER_CONSTRAINT } from "./tokens"; +export type { TokenByChain, TokenChainEntry, TokenLinks, TokenTransferConstraint } from "./tokens"; diff --git a/src/tokens/data/base-sepolia.json b/src/tokens/data/base-sepolia.json index 37b6028..1eaf6f1 100644 --- a/src/tokens/data/base-sepolia.json +++ b/src/tokens/data/base-sepolia.json @@ -14,7 +14,7 @@ { "symbol": "USDT", "address": "0xCE8565457Cca0fC7542608A2c78610Ed7bC66C8C", - "decimals": 6, + "decimals": 18, "name": "Tether USD" }, { diff --git a/src/tokens/data/base.json b/src/tokens/data/base.json index 1c5e5da..3844f35 100644 --- a/src/tokens/data/base.json +++ b/src/tokens/data/base.json @@ -119,6 +119,27 @@ "coinmarketcap": "https://coinmarketcap.com/currencies/degen-base/" } }, + { + "symbol": "ezETH", + "address": "0x2416092f143378750bb29b79eD961ab195CcEea5", + "decimals": 18, + "name": "Renzo Restaked ETH", + "explorer": "https://basescan.org/token/0x2416092f143378750bb29b79eD961ab195CcEea5" + }, + { + "symbol": "GHO", + "address": "0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee", + "decimals": 18, + "name": "GHO", + "explorer": "https://basescan.org/token/0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee" + }, + { + "symbol": "LBTC", + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1", + "decimals": 8, + "name": "Lombard Staked Bitcoin", + "explorer": "https://basescan.org/token/0xecAc9C5F704e954931349Da37F60E39f515c11c1" + }, { "symbol": "LINK", "address": "0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196", @@ -152,6 +173,13 @@ "coingecko": "https://coingecko.com/en/coins/rocket-pool-eth/" } }, + { + "symbol": "RLUSD", + "address": "0x8d58C0C60B8D6b88Fa98B291a646dB34d0F98258", + "decimals": 18, + "name": "Ripple USD", + "explorer": "https://basescan.org/token/0x8d58C0C60B8D6b88Fa98B291a646dB34d0F98258" + }, { "symbol": "RSR", "address": "0xaB36452DbAC151bE02b16Ca17d8919826072f64a", @@ -165,6 +193,34 @@ "telegram": "https://t.me/reservecurrency" } }, + { + "symbol": "sUSDe", + "address": "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "decimals": 18, + "name": "Staked USDe", + "explorer": "https://basescan.org/token/0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2" + }, + { + "symbol": "sUSDS", + "address": "0x5875eEE11Cf8398102FdAd704C9E96607675467a", + "decimals": 18, + "name": "Savings USDS", + "explorer": "https://basescan.org/token/0x5875eEE11Cf8398102FdAd704C9E96607675467a" + }, + { + "symbol": "syrupUSDC", + "address": "0x660975730059246A68521a3e2FBD4740173100f5", + "decimals": 6, + "name": "Syrup USDC", + "explorer": "https://basescan.org/token/0x660975730059246A68521a3e2FBD4740173100f5" + }, + { + "symbol": "tBTC", + "address": "0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b", + "decimals": 18, + "name": "Base tBTC v2", + "explorer": "https://basescan.org/token/0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b" + }, { "symbol": "TOSHI", "address": "0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4", @@ -220,6 +276,20 @@ "coingecko": "https://coingecko.com/en/coins/usd-coin/" } }, + { + "symbol": "USDe", + "address": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "decimals": 18, + "name": "USDe", + "explorer": "https://basescan.org/token/0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34" + }, + { + "symbol": "USDS", + "address": "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc", + "decimals": 18, + "name": "USDS Stablecoin", + "explorer": "https://basescan.org/token/0x820C137fa70C8691f0e44Dc420a5e53c168921Dc" + }, { "symbol": "USDT", "address": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", @@ -241,6 +311,20 @@ "website": "https://app.virtuals.io/", "explorer": "https://basescan.org/token/0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b" }, + { + "symbol": "WBTC", + "address": "0x1ceA84203673764244E05693e42E6Ace62bE9BA5", + "decimals": 8, + "name": "Wrapped BTC", + "explorer": "https://basescan.org/token/0x1ceA84203673764244E05693e42E6Ace62bE9BA5" + }, + { + "symbol": "weETH", + "address": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A", + "decimals": 18, + "name": "Wrapped eETH", + "explorer": "https://basescan.org/token/0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A" + }, { "symbol": "WELL", "address": "0xA88594D404727625A9437C3f886C7643872296AE", @@ -266,6 +350,13 @@ "coingecko": "https://coingecko.com/coins/weth/" } }, + { + "symbol": "wrsETH", + "address": "0xEDfa23602D0EC14714057867A78d01e94176BEA0", + "decimals": 18, + "name": "Wrapped rsETH", + "explorer": "https://basescan.org/token/0xEDfa23602D0EC14714057867A78d01e94176BEA0" + }, { "symbol": "wstETH", "address": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", diff --git a/src/tokens/data/bnb-mainnet.json b/src/tokens/data/bnb-mainnet.json index 9599ceb..1f3dedf 100644 --- a/src/tokens/data/bnb-mainnet.json +++ b/src/tokens/data/bnb-mainnet.json @@ -1,4 +1,81 @@ [ + { + "symbol": "BNBx", + "address": "0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275", + "decimals": 18, + "name": "Liquid Staking BNB", + "explorer": "https://bscscan.com/token/0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275" + }, + { + "symbol": "BTCB", + "address": "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", + "decimals": 18, + "name": "Binance-Peg BTCB Token", + "explorer": "https://bscscan.com/token/0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c" + }, + { + "symbol": "Cake", + "address": "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82", + "decimals": 18, + "name": "PancakeSwap Token", + "explorer": "https://bscscan.com/token/0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82" + }, + { + "symbol": "ETH", + "address": "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", + "decimals": 18, + "name": "Binance-Peg Ethereum Token", + "explorer": "https://bscscan.com/token/0x2170Ed0880ac9A755fd29B2688956BD959F933F8" + }, + { + "symbol": "FDUSD", + "address": "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", + "decimals": 18, + "name": "First Digital USD", + "explorer": "https://bscscan.com/token/0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409" + }, + { + "symbol": "LBTC", + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1", + "decimals": 8, + "name": "Lombard Staked Bitcoin", + "explorer": "https://bscscan.com/token/0xecAc9C5F704e954931349Da37F60E39f515c11c1" + }, + { + "symbol": "LINK", + "address": "0x404460C6A5EdE2D891e8297795264fDe62ADBB75", + "decimals": 18, + "name": "Chainlink", + "explorer": "https://bscscan.com/token/0x404460C6A5EdE2D891e8297795264fDe62ADBB75" + }, + { + "symbol": "slisBNB", + "address": "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B", + "decimals": 18, + "name": "Staked Lista BNB", + "explorer": "https://bscscan.com/token/0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" + }, + { + "symbol": "SolvBTC", + "address": "0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7", + "decimals": 18, + "name": "Solv BTC", + "explorer": "https://bscscan.com/token/0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7" + }, + { + "symbol": "sUSDe", + "address": "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "decimals": 18, + "name": "Staked USDe", + "explorer": "https://bscscan.com/token/0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2" + }, + { + "symbol": "USD1", + "address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", + "decimals": 18, + "name": "World Liberty Financial USD", + "explorer": "https://bscscan.com/token/0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d" + }, { "symbol": "USDC", "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", @@ -6,11 +83,39 @@ "name": "Binance-Peg USD Coin", "explorer": "https://bscscan.com/token/0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" }, + { + "symbol": "USDe", + "address": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "decimals": 18, + "name": "USDe", + "explorer": "https://bscscan.com/token/0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34" + }, { "symbol": "USDT", "address": "0x55d398326f99059fF775485246999027B3197955", "decimals": 18, "name": "Binance-Peg Tether USD", "explorer": "https://bscscan.com/token/0x55d398326f99059fF775485246999027B3197955" + }, + { + "symbol": "WBNB", + "address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + "decimals": 18, + "name": "Wrapped BNB", + "explorer": "https://bscscan.com/token/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" + }, + { + "symbol": "wstETH", + "address": "0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C", + "decimals": 18, + "name": "Wrapped liquid staked Ether 2.0", + "explorer": "https://bscscan.com/token/0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C" + }, + { + "symbol": "XVS", + "address": "0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63", + "decimals": 18, + "name": "Venus", + "explorer": "https://bscscan.com/token/0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63" } ] diff --git a/src/tokens/data/ethereum.json b/src/tokens/data/ethereum.json index d47f97c..82a92f2 100644 --- a/src/tokens/data/ethereum.json +++ b/src/tokens/data/ethereum.json @@ -1,4 +1,11 @@ [ + { + "symbol": "1INCH", + "address": "0x111111111117dC0aa78b770fA6A738034120C302", + "decimals": 18, + "name": "1inch", + "explorer": "https://etherscan.io/token/0x111111111117dC0aa78b770fA6A738034120C302" + }, { "symbol": "AAVE", "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", @@ -57,6 +64,13 @@ "decimals": 18, "name": "Axie Infinity Shard" }, + { + "symbol": "BAL", + "address": "0xba100000625a3754423978a60c9317c58a424e3D", + "decimals": 18, + "name": "Balancer", + "explorer": "https://etherscan.io/token/0xba100000625a3754423978a60c9317c58a424e3D" + }, { "symbol": "BGB", "address": "0x19de6b897Ed14A376Dda0Fe53a5420D2aC828a28", @@ -75,6 +89,13 @@ "decimals": 5, "name": "Bonk" }, + { + "symbol": "BTC.b", + "address": "0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072", + "decimals": 8, + "name": "BTC.b (Lombard)", + "explorer": "https://etherscan.io/token/0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072" + }, { "symbol": "BTT", "address": "0xC669928185DbCE49d2230CC9B0979BE6DC797957", @@ -85,7 +106,10 @@ "symbol": "BUIDL", "address": "0x7712c34205737192402172409a8F7ccef8aA2AEc", "decimals": 6, - "name": "BlackRock USD Institutional Digital Liquidity Fund" + "name": "BlackRock USD Institutional Digital Liquidity Fund", + "transferConstraints": [ + "permissionedRecipient" + ] }, { "symbol": "Cake", @@ -99,6 +123,13 @@ "decimals": 8, "name": "Coinbase Wrapped BTC" }, + { + "symbol": "cbETH", + "address": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", + "decimals": 18, + "name": "Coinbase Wrapped Staked ETH", + "explorer": "https://etherscan.io/token/0xBe9895146f7AF43049ca1c1AE358B0541Ea49704" + }, { "symbol": "cgETH.hashkey", "address": "0xc60a9145D9e9f1152218e7da6df634b7a74aE444", @@ -152,6 +183,13 @@ "coingecko": "https://coingecko.com/en/coins/curve-dao-token/" } }, + { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", + "decimals": 18, + "name": "Curve.Fi USD Stablecoin", + "explorer": "https://etherscan.io/token/0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + }, { "symbol": "DAI", "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", @@ -188,11 +226,21 @@ "github": "https://github.com/dydxfoundation" } }, + { + "symbol": "eBTC", + "address": "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", + "decimals": 8, + "name": "ether.fi BTC", + "explorer": "https://etherscan.io/token/0x657e8C867D8B37dCC18fA4Caead9C45EB088C642" + }, { "symbol": "eETH", "address": "0x35fA164735182de50811E8e2E824cFb9B6118ac2", "decimals": 18, - "name": "ether.fi ETH" + "name": "ether.fi ETH", + "transferConstraints": [ + "rebasing" + ] }, { "symbol": "EIGEN", @@ -241,6 +289,34 @@ "decimals": 18, "name": "ether.fi governance token" }, + { + "symbol": "ETHx", + "address": "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b", + "decimals": 18, + "name": "Stader ETHx", + "explorer": "https://etherscan.io/token/0xA35b1B31Ce002FBF2058D22F30f95D405200A15b" + }, + { + "symbol": "eUSDe", + "address": "0x90D2af7d622ca3141efA4d8f1F24d86E5974Cc8F", + "decimals": 18, + "name": "Ethereal Pre-deposit Vault", + "explorer": "https://etherscan.io/token/0x90D2af7d622ca3141efA4d8f1F24d86E5974Cc8F" + }, + { + "symbol": "ezETH", + "address": "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + "decimals": 18, + "name": "Renzo Restaked ETH", + "explorer": "https://etherscan.io/token/0xbf5495Efe5DB9ce00f80364C8B423567e58d2110" + }, + { + "symbol": "FBTC", + "address": "0xC96dE26018A54D51c097160568752c4E3BD6C364", + "decimals": 8, + "name": "Fire Bitcoin", + "explorer": "https://etherscan.io/token/0xC96dE26018A54D51c097160568752c4E3BD6C364" + }, { "symbol": "FDUSD", "address": "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", @@ -277,18 +353,39 @@ "discord": "https://discord.com/invite/Xwe8kAwZ4a" } }, + { + "symbol": "frxETH", + "address": "0x5E8422345238F34275888049021821E8E08CAa1f", + "decimals": 18, + "name": "Frax Ether", + "explorer": "https://etherscan.io/token/0x5E8422345238F34275888049021821E8E08CAa1f" + }, { "symbol": "FTN", "address": "0xaeDf386B755465871fF874E3E37Af5976E247064", "decimals": 18, "name": "Fasttoken" }, + { + "symbol": "FXS", + "address": "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0", + "decimals": 18, + "name": "Frax Share", + "explorer": "https://etherscan.io/token/0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0" + }, { "symbol": "GALA", "address": "0xd1d2Eb1B1e90B638588728b4130137D262C87cae", "decimals": 8, "name": "Gala" }, + { + "symbol": "GHO", + "address": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "decimals": 18, + "name": "GHO", + "explorer": "https://etherscan.io/token/0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f" + }, { "symbol": "GRT", "address": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", @@ -319,6 +416,13 @@ "decimals": 18, "name": "JasmyCoin" }, + { + "symbol": "KNC", + "address": "0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202", + "decimals": 18, + "name": "Kyber Network Crystal v2", + "explorer": "https://etherscan.io/token/0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202" + }, { "symbol": "LBTC", "address": "0x8236a87084f8B84306f72007F36F2618A5634494", @@ -367,6 +471,13 @@ "decimals": 18, "name": "Liquid Staked ETH" }, + { + "symbol": "LUSD", + "address": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", + "decimals": 18, + "name": "LUSD Stablecoin", + "explorer": "https://etherscan.io/token/0x5f98805A4E8be255a32880FDeC7F6728C6568bA0" + }, { "symbol": "MANA", "address": "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", @@ -400,6 +511,13 @@ "decimals": 18, "name": "Mantle" }, + { + "symbol": "mUSD", + "address": "0xacA92E438df0B2401fF60dA7E4337B687a2435DA", + "decimals": 6, + "name": "MetaMask USD", + "explorer": "https://etherscan.io/token/0xacA92E438df0B2401fF60dA7E4337B687a2435DA" + }, { "symbol": "NEAR", "address": "0x85F17Cf997934a597031b2E18a9aB6ebD4B9f6a4", @@ -440,7 +558,10 @@ "symbol": "OUSG", "address": "0x1B19C19393e2d034D8Ff31ff34c81252FcBbee92", "decimals": 18, - "name": "Ondo Short-Term U.S. Government Bond Fund" + "name": "Ondo Short-Term U.S. Government Bond Fund", + "transferConstraints": [ + "permissionedRecipient" + ] }, { "symbol": "PAXG", @@ -520,6 +641,13 @@ "decimals": 18, "name": "Rocket Pool ETH" }, + { + "symbol": "RLUSD", + "address": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD", + "decimals": 18, + "name": "Ripple USD", + "explorer": "https://etherscan.io/token/0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD" + }, { "symbol": "RNDR", "address": "0x6De037ef9aD2725EB40118Bb1702EBb27e4Aeb24", @@ -563,6 +691,20 @@ "decimals": 18, "name": "SAND" }, + { + "symbol": "sDAI", + "address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "decimals": 18, + "name": "Savings Dai", + "explorer": "https://etherscan.io/token/0x83F20F44975D03b1b09e64809B757c47f942BEeA" + }, + { + "symbol": "sfrxETH", + "address": "0xac3E018457B222d93114458476f3E3416Abbe38F", + "decimals": 18, + "name": "Staked Frax Ether", + "explorer": "https://etherscan.io/token/0xac3E018457B222d93114458476f3E3416Abbe38F" + }, { "symbol": "SHIB", "address": "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", @@ -577,6 +719,13 @@ "coingecko": "https://coingecko.com/en/coins/shiba-inu/" } }, + { + "symbol": "SKY", + "address": "0x56072C95FAA701256059aa122697B133aDEd9279", + "decimals": 18, + "name": "SKY Governance Token", + "explorer": "https://etherscan.io/token/0x56072C95FAA701256059aa122697B133aDEd9279" + }, { "symbol": "SNX", "address": "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F", @@ -590,10 +739,10 @@ "name": "Solv BTC" }, { - "symbol": "SolvBTC.BBN", + "symbol": "xSolvBTC", "address": "0xd9D920AA40f578ab794426F5C90F6C731D159DEf", "decimals": 18, - "name": "SolvBTC Babylon" + "name": "xSolvBTC" }, { "symbol": "SPX", @@ -608,7 +757,17 @@ "name": "stETH", "description": "stETH is a token that represents staked ether in Lido.", "website": "https://stake.lido.fi", - "explorer": "https://etherscan.io/token/0xae7ab96520de3a18e5e111b5eaab095312d7fe84" + "explorer": "https://etherscan.io/token/0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "transferConstraints": [ + "rebasing" + ] + }, + { + "symbol": "STG", + "address": "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", + "decimals": 18, + "name": "Stargate", + "explorer": "https://etherscan.io/token/0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" }, { "symbol": "STRK", @@ -640,6 +799,13 @@ "decimals": 6, "name": "Syrup USDC" }, + { + "symbol": "syrupUSDT", + "address": "0x356B8d89c1e1239Cbbb9dE4815c39A1474d5BA7D", + "decimals": 6, + "name": "Syrup USDT", + "explorer": "https://etherscan.io/token/0x356B8d89c1e1239Cbbb9dE4815c39A1474d5BA7D" + }, { "symbol": "tBTC", "address": "0x18084fbA666a33d37592fA2633fD49a74DD93a88", @@ -652,6 +818,13 @@ "decimals": 2, "name": "Telcoin" }, + { + "symbol": "tETH", + "address": "0xD11c452fc99cF405034ee446803b6F6c1F6d5ED8", + "decimals": 18, + "name": "Treehouse ETH", + "explorer": "https://etherscan.io/token/0xD11c452fc99cF405034ee446803b6F6c1F6d5ED8" + }, { "symbol": "TKX", "address": "0x667102BD3413bFEaa3Dffb48fa8288819E480a88", @@ -716,6 +889,13 @@ "decimals": 18, "name": "USDe" }, + { + "symbol": "USDG", + "address": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", + "decimals": 6, + "name": "Global Dollar", + "explorer": "https://etherscan.io/token/0xe343167631d89B6Ffc58B88d6b7fB0228795491D" + }, { "symbol": "USDS", "address": "0xdC035D45d973E3EC169d2276DDab16f1e407384F", @@ -736,6 +916,13 @@ "coinmarketcap": "https://coinmarketcap.com/ru/currencies/tether/" } }, + { + "symbol": "USDtb", + "address": "0xC139190F447e929f090Edeb554D95AbB8b18aC1C", + "decimals": 18, + "name": "USDtb", + "explorer": "https://etherscan.io/token/0xC139190F447e929f090Edeb554D95AbB8b18aC1C" + }, { "symbol": "USDX", "address": "0xf3527ef8dE265eAa3716FB312c12847bFBA66Cef", @@ -752,7 +939,10 @@ "symbol": "USYC", "address": "0x136471a34f6ef19fE571EFFC1CA711fdb8E49f2b", "decimals": 6, - "name": "US Yield Coin" + "name": "US Yield Coin", + "transferConstraints": [ + "permissionedRecipient" + ] }, { "symbol": "VIRTUAL", diff --git a/src/tokens/index.ts b/src/tokens/index.ts index 9ff88ec..7a0b5c9 100644 --- a/src/tokens/index.ts +++ b/src/tokens/index.ts @@ -46,7 +46,8 @@ import * as base from "./base"; import * as baseSepolia from "./base-sepolia"; import * as bnbMainnet from "./bnb-mainnet"; -export type { TokenByChain, TokenChainEntry, TokenLinks } from "./types"; +export { TOKEN_TRANSFER_CONSTRAINT } from "./types"; +export type { TokenByChain, TokenChainEntry, TokenLinks, TokenTransferConstraint } from "./types"; /** * Per-chain module list. Each module exposes `chainId` plus a frozen diff --git a/src/tokens/provenance/base-sepolia.json b/src/tokens/provenance/base-sepolia.json new file mode 100644 index 0000000..3e1fe4d --- /dev/null +++ b/src/tokens/provenance/base-sepolia.json @@ -0,0 +1,42 @@ +[ + { + "symbol": "EURC", + "address": "0x808456652fdb597867f38412077A9182bf77359F", + "issuance": "test", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/eurc-contract-addresses" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "LINK", + "address": "0xE4aB69C077896252FAFBD49EFD26B5D171A32410", + "issuance": "test", + "issuer": "Chainlink", + "sources": [ + "https://docs.chain.link/resources/link-token-contracts" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDC", + "address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "issuance": "test", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/usdc-contract-addresses" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "issuance": "test", + "issuer": "OP Stack WETH9 predeploy", + "sources": [ + "https://docs.base.org/base-chain/network-information/base-contracts" + ], + "verifiedAt": "2026-10-28" + } +] diff --git a/src/tokens/provenance/base.json b/src/tokens/provenance/base.json new file mode 100644 index 0000000..f01d014 --- /dev/null +++ b/src/tokens/provenance/base.json @@ -0,0 +1,332 @@ +[ + { + "symbol": "AAVE", + "address": "0x63706e401c06ac8513145b7687A14804d17f814b", + "issuance": "bridged", + "issuer": "Aave", + "bridge": "Base standard bridge", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "On-chain REMOTE_TOKEN()/l1Token() return the L1 token and BRIDGE() returns the OP-stack standard bridge 0x4200…0010." + }, + { + "symbol": "AERO", + "address": "0x940181a94A35A4569E4529A3CDfB74e38FD98631", + "issuance": "native", + "issuer": "Aerodrome", + "sources": [ + "https://github.com/aerodrome-finance/contracts/blob/main/README.md" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "cbBTC", + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "issuance": "native", + "issuer": "Coinbase", + "sources": [ + "https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Same address as on Ethereum." + }, + { + "symbol": "cbETH", + "address": "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", + "issuance": "bridged", + "issuer": "Coinbase", + "bridge": "Base standard bridge", + "sources": [ + "https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "On-chain REMOTE_TOKEN()/l1Token() return the L1 token and BRIDGE() returns the OP-stack standard bridge 0x4200…0010." + }, + { + "symbol": "DAI", + "address": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + "issuance": "bridged", + "issuer": "Base standard bridge (not Sky)", + "bridge": "Base standard bridge", + "sources": [], + "references": [ + "https://github.com/ethereum-optimism/ethereum-optimism.github.io" + ], + "verifiedAt": "2026-10-28", + "notes": "Sky lists no Base DAI. On-chain REMOTE_TOKEN()/l1Token() return the L1 token and BRIDGE() returns the OP-stack standard bridge 0x4200…0010." + }, + { + "symbol": "EURC", + "address": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42", + "issuance": "native", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/eurc-contract-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "ezETH", + "address": "0x2416092f143378750bb29b79eD961ab195CcEea5", + "issuance": "bridged", + "issuer": "Renzo", + "bridge": "Renzo xERC20 (Connext)", + "sources": [ + "https://docs.renzoprotocol.com/docs/contracts/layer-2s/base" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "GHO", + "address": "0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee", + "issuance": "native", + "issuer": "Aave", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/GhoBase.sol", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Moves across chains through Aave's CCIP token pool." + }, + { + "symbol": "LBTC", + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1", + "issuance": "native", + "issuer": "Lombard", + "sources": [ + "https://docs.lombard.finance/learn/transparency/smart-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Cross-chain moves are Chainlink CCIP burn-and-mint co-signed by Lombard." + }, + { + "symbol": "LINK", + "address": "0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196", + "issuance": "native", + "issuer": "Chainlink", + "sources": [ + "https://docs.chain.link/resources/link-token-contracts" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "rETH", + "address": "0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c", + "issuance": "bridged", + "issuer": "Rocket Pool", + "bridge": "Base standard bridge", + "sources": [ + "https://github.com/rocket-pool/docs.rocketpool.net/blob/main/docs/en/protocol/contracts-integrations.md" + ], + "verifiedAt": "2026-10-28", + "notes": "On-chain REMOTE_TOKEN()/l1Token() return the L1 token and BRIDGE() returns the OP-stack standard bridge 0x4200…0010." + }, + { + "symbol": "RLUSD", + "address": "0x8d58C0C60B8D6b88Fa98B291a646dB34d0F98258", + "issuance": "native", + "issuer": "Ripple", + "sources": [ + "https://docs.ripple.com/products/stablecoin/overview/token-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Ripple lists the same proxy address on Base, Ink, Optimism, Unichain and XRPL EVM." + }, + { + "symbol": "sUSDe", + "address": "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "issuance": "native", + "issuer": "Ethena", + "sources": [ + "https://docs.ethena.fi/solution-design/key-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Ethena's \"most L2s\" address; Base is in the listed chains." + }, + { + "symbol": "sUSDS", + "address": "0x5875eEE11Cf8398102FdAd704C9E96607675467a", + "issuance": "bridged", + "issuer": "Sky", + "bridge": "Sky Base token bridge", + "sources": [ + "https://github.com/sky-ecosystem/spells-mainnet/blob/master/src/test/addresses_base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Key L2_SUSDS in Sky's Base address file." + }, + { + "symbol": "syrupUSDC", + "address": "0x660975730059246A68521a3e2FBD4740173100f5", + "issuance": "native", + "issuer": "Maple", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "tBTC", + "address": "0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b", + "issuance": "bridged", + "issuer": "Threshold", + "bridge": "Wormhole (Threshold BaseWormholeGateway)", + "sources": [ + "https://docs.threshold.network/contract-addresses/tbtc" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDbC", + "address": "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA", + "issuance": "bridged", + "issuer": "Base standard bridge (not Circle)", + "bridge": "Base standard bridge", + "legacy": true, + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Bridged USDC from before native USDC launched on Base. On-chain REMOTE_TOKEN()/l1Token() return the L1 token and BRIDGE() returns the OP-stack standard bridge 0x4200…0010." + }, + { + "symbol": "USDC", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "issuance": "native", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/usdc-contract-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDe", + "address": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "issuance": "native", + "issuer": "Ethena", + "sources": [ + "https://docs.ethena.fi/solution-design/key-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Ethena's \"most L2s\" address; Base is in the listed chains." + }, + { + "symbol": "USDS", + "address": "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc", + "issuance": "bridged", + "issuer": "Sky", + "bridge": "Sky Base token bridge", + "sources": [ + "https://github.com/sky-ecosystem/spells-mainnet/blob/master/src/test/addresses_base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Key L2_USDS in Sky's Base address file." + }, + { + "symbol": "USDT", + "address": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", + "issuance": "bridged", + "issuer": "Base standard bridge (not Tether)", + "bridge": "Base standard bridge", + "sources": [], + "references": [ + "https://github.com/ethereum-optimism/ethereum-optimism.github.io" + ], + "verifiedAt": "2026-10-28", + "notes": "Tether lists no Base deployment. On-chain REMOTE_TOKEN()/l1Token() return the L1 token and BRIDGE() returns the OP-stack standard bridge 0x4200…0010." + }, + { + "symbol": "WBTC", + "address": "0x1ceA84203673764244E05693e42E6Ace62bE9BA5", + "issuance": "native", + "issuer": "BitGo", + "sources": [ + "https://wbtc.network/transparency", + "https://www.bitgo.com/resources/blog/launching-wrapped-bitcoin-wbtc-on-base" + ], + "verifiedAt": "2026-10-28", + "notes": "Supply was small at verification (about 0.005 WBTC)." + }, + { + "symbol": "weETH", + "address": "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A", + "issuance": "bridged", + "issuer": "ether.fi", + "bridge": "LayerZero OFT", + "sources": [ + "https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "The Superchain token list carries a different Base weETH (0x4c94…5EFA) that ether.fi does not list." + }, + { + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "issuance": "native", + "issuer": "OP Stack WETH9 predeploy", + "sources": [ + "https://docs.base.org/base-chain/network-information/base-contracts", + "https://specs.optimism.io/protocol/predeploys.html" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "wrsETH", + "address": "0xEDfa23602D0EC14714057867A78d01e94176BEA0", + "issuance": "bridged", + "issuer": "Kelp", + "bridge": "Kelp rsETH wrapper", + "sources": [ + "https://github.com/Kelp-DAO/LRT-rsETH/blob/main/README.md" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Kelp's LayerZero rsETH on Base (0x1Bc7…5993) is a separate token." + }, + { + "symbol": "wstETH", + "address": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + "issuance": "bridged", + "issuer": "Lido", + "bridge": "Lido token bridge", + "sources": [ + "https://docs.lido.fi/deployed-contracts/" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Base.sol" + ], + "verifiedAt": "2026-10-28" + } +] diff --git a/src/tokens/provenance/bnb-mainnet.json b/src/tokens/provenance/bnb-mainnet.json new file mode 100644 index 0000000..3c363d4 --- /dev/null +++ b/src/tokens/provenance/bnb-mainnet.json @@ -0,0 +1,240 @@ +[ + { + "symbol": "BNBx", + "address": "0x1bdd3Cf7F79cfB8EdbB955f20ad99211551BA275", + "issuance": "native", + "issuer": "Stader", + "legacy": true, + "sources": [ + "https://stader.gitbook.io/stader/bnb-chain-discontinued" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json" + ], + "verifiedAt": "2026-10-28", + "notes": "Stader discontinued BNB staking: deposits disabled 2026-01-29, dApp 2026-06-09. Holders unstake on the contract directly." + }, + { + "symbol": "BTCB", + "address": "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", + "issuance": "bridged", + "issuer": "Binance", + "bridge": "Binance-Peg", + "sources": [ + "https://github.com/bnb-chain/bcfusion/blob/main/README.md" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Bound to BEP2 BTCB-1DE." + }, + { + "symbol": "Cake", + "address": "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82", + "issuance": "native", + "issuer": "PancakeSwap", + "sources": [ + "https://docs.pancakeswap.finance/protocol/cake-tokenomics" + ], + "references": [ + "https://github.com/bnb-chain/bcfusion/blob/main/README.md", + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "ETH", + "address": "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", + "issuance": "bridged", + "issuer": "Binance", + "bridge": "Binance-Peg", + "sources": [ + "https://github.com/bnb-chain/bcfusion/blob/main/README.md" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Bound to BEP2 ETH-1C9. An ERC-20, not native ETH." + }, + { + "symbol": "FDUSD", + "address": "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", + "issuance": "native", + "issuer": "First Digital", + "sources": [ + "https://www.firstdigitallabs.com/fdusd" + ], + "references": [ + "https://web.archive.org/web/20260417165942/https://www.firstdigitallabs.com/fdusd", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Same address as on Ethereum. The live page returned 403 on 2026-10-28; the address was read from its 2026-04-17 archive." + }, + { + "symbol": "LBTC", + "address": "0xecAc9C5F704e954931349Da37F60E39f515c11c1", + "issuance": "native", + "issuer": "Lombard", + "sources": [ + "https://docs.lombard.finance/learn/transparency/smart-contracts" + ], + "verifiedAt": "2026-10-28", + "notes": "Lombard lists the same address on BNB Smart Chain as on Base." + }, + { + "symbol": "LINK", + "address": "0x404460C6A5EdE2D891e8297795264fDe62ADBB75", + "issuance": "native", + "issuer": "Chainlink", + "sources": [ + "https://docs.chain.link/resources/link-token-contracts" + ], + "verifiedAt": "2026-10-28", + "notes": "Chainlink notes LINK bridged through the BNB Chain bridge is not ERC-677 compatible until swapped via PegSwap." + }, + { + "symbol": "slisBNB", + "address": "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B", + "issuance": "native", + "issuer": "Lista DAO", + "sources": [ + "https://docs.bsc.lista.org/for-developer/liquid-staking-slisbnb/smart-contract" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "SolvBTC", + "address": "0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7", + "issuance": "native", + "issuer": "Solv", + "sources": [ + "https://github.com/solv-finance/SolvBTC/blob/main/deploy/SolvBTC/10099_export_SolvBTCInfos.js" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json" + ], + "verifiedAt": "2026-10-28", + "notes": "docs.solv.finance returned 403; source is Solv's deploy script on GitHub." + }, + { + "symbol": "sUSDe", + "address": "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "issuance": "native", + "issuer": "Ethena", + "sources": [ + "https://docs.ethena.fi/solution-design/key-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Ethena's page says L2 addresses are identical on every listed chain except Solana, ZKSync and Zircuit; BNB is listed." + }, + { + "symbol": "USD1", + "address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", + "issuance": "native", + "issuer": "World Liberty Financial", + "sources": [ + "https://docs.worldlibertyfinancial.com/usd1-token/contract-addresses" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json" + ], + "verifiedAt": "2026-10-28", + "notes": "Same address as on Ethereum." + }, + { + "symbol": "USDC", + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "issuance": "bridged", + "issuer": "Binance (not Circle)", + "bridge": "Binance-Peg", + "sources": [ + "https://github.com/bnb-chain/bcfusion/blob/main/README.md" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Bound to BEP2 USDC-CD2. Circle lists no BNB Chain USDC. 18 decimals; on-chain name \"USD Coin\"." + }, + { + "symbol": "USDe", + "address": "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + "issuance": "native", + "issuer": "Ethena", + "sources": [ + "https://docs.ethena.fi/solution-design/key-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Ethena's page says L2 addresses are identical on every listed chain except Solana, ZKSync and Zircuit; BNB is listed." + }, + { + "symbol": "USDT", + "address": "0x55d398326f99059fF775485246999027B3197955", + "issuance": "bridged", + "issuer": "Binance (not Tether)", + "bridge": "Binance-Peg", + "sources": [ + "https://github.com/bnb-chain/bcfusion/blob/main/README.md" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Bound to BEP2 USDT-6D8. Tether lists no BNB Chain USDT. 18 decimals, against 6 on Ethereum." + }, + { + "symbol": "WBNB", + "address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + "issuance": "native", + "issuer": "BNB Chain", + "sources": [ + "https://github.com/bnb-chain/bnb-chain.github.io/blob/master/docs/bnb-smart-chain/benchmark/design-reference.md", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json" + ], + "verifiedAt": "2026-10-28", + "notes": "No dedicated BNB Chain address registry; BNB Chain's docs link the contract and Aave lists it as a Core reserve. Also Protocols.wrapped.weth[56]." + }, + { + "symbol": "wstETH", + "address": "0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C", + "issuance": "bridged", + "issuer": "Lido", + "bridge": "Wormhole NTT", + "sources": [ + "https://docs.lido.fi/deployed-contracts/" + ], + "references": [ + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3BNB.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "XVS", + "address": "0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63", + "issuance": "native", + "issuer": "Venus", + "sources": [ + "https://docs-v4.venus.io/deployed-contracts/xvs-omnichain" + ], + "references": [ + "https://github.com/bnb-chain/bcfusion/blob/main/README.md", + "https://github.com/pancakeswap/token-list/blob/main/src/tokens/pancakeswap-extended.json" + ], + "verifiedAt": "2026-10-28" + } +] diff --git a/src/tokens/provenance/ethereum.json b/src/tokens/provenance/ethereum.json new file mode 100644 index 0000000..5cf5384 --- /dev/null +++ b/src/tokens/provenance/ethereum.json @@ -0,0 +1,774 @@ +[ + { + "symbol": "1INCH", + "address": "0x111111111117dC0aa78b770fA6A738034120C302", + "issuance": "native", + "issuer": "1inch", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "AAVE", + "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", + "issuance": "native", + "issuer": "Aave", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "BAL", + "address": "0xba100000625a3754423978a60c9317c58a424e3D", + "issuance": "native", + "issuer": "Balancer", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "BTC.b", + "address": "0xB0F70C0bD6FD87dbEb7C10dC692a2a6106817072", + "issuance": "native", + "issuer": "Lombard", + "sources": [ + "https://docs.lombard.finance/learn/transparency/smart-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Launched by Ava Labs; acquired by Lombard in October 2025. 1:1 BTC wrapper, no yield." + }, + { + "symbol": "cbBTC", + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "issuance": "native", + "issuer": "Coinbase", + "sources": [ + "https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "coinbase.com returned 403; source is Coinbase's official GitHub." + }, + { + "symbol": "cbETH", + "address": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", + "issuance": "native", + "issuer": "Coinbase", + "sources": [ + "https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/action-providers/erc20/constants.ts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "coinbase.com returned 403; source is Coinbase's official GitHub." + }, + { + "symbol": "cmETH", + "address": "0xE6829d9a7eE3040e1276Fa75293Bde931859e8fA", + "issuance": "native", + "issuer": "Mantle", + "sources": [ + "https://docs.mantle.xyz/meth/components/smart-contracts/restaking-cmeth" + ], + "verifiedAt": "2026-10-28", + "notes": "A secondary report says minting was disabled on 2026-05-07 as Mantle winds cmETH down; no Mantle announcement was fetched. Transfers simulated cleanly on 2026-10-28." + }, + { + "symbol": "CRV", + "address": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "issuance": "native", + "issuer": "Curve", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "crvUSD", + "address": "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", + "issuance": "native", + "issuer": "Curve", + "sources": [ + "https://docs.curve.finance/developer/crvusd/" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "DAI", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "issuance": "native", + "issuer": "Sky", + "sources": [ + "https://chainlog.sky.money/api/mainnet/active.json" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Chainlog key MCD_DAI." + }, + { + "symbol": "eBTC", + "address": "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", + "issuance": "native", + "issuer": "ether.fi", + "sources": [ + "https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "eETH", + "address": "0x35fA164735182de50811E8e2E824cFb9B6118ac2", + "issuance": "native", + "issuer": "ether.fi", + "sources": [ + "https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts" + ], + "verifiedAt": "2026-10-28", + "notes": "Exists only on Ethereum. Rebasing; see transferConstraints." + }, + { + "symbol": "ENS", + "address": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + "issuance": "native", + "issuer": "ENS", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "ETHx", + "address": "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b", + "issuance": "native", + "issuer": "Stader", + "sources": [ + "https://staderlabs.gitbook.io/ethereum/smart-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "EURC", + "address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", + "issuance": "native", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/eurc-contract-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "eUSDe", + "address": "0x90D2af7d622ca3141efA4d8f1F24d86E5974Cc8F", + "issuance": "native", + "issuer": "Ethereal", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "No Ethena or Ethereal page lists it. Source is Aave's address book, which lists it as a Core reserve; on-chain name is \"Ethereal Pre-deposit Vault\"." + }, + { + "symbol": "ezETH", + "address": "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + "issuance": "native", + "issuer": "Renzo", + "sources": [ + "https://docs.renzoprotocol.com/docs/contracts/ethereum-mainnet" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Renzo also lists an L1 xERC20 ezETH at 0x2416…ceea5, a separate token; that address is the catalog's Base ezETH." + }, + { + "symbol": "FBTC", + "address": "0xC96dE26018A54D51c097160568752c4E3BD6C364", + "issuance": "native", + "issuer": "Ignition (FBTC)", + "sources": [ + "https://docs.fbtc.com/developers/smart-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "FDUSD", + "address": "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", + "issuance": "native", + "issuer": "First Digital", + "sources": [ + "https://www.firstdigitallabs.com/fdusd" + ], + "references": [ + "https://web.archive.org/web/20260417165942/https://www.firstdigitallabs.com/fdusd" + ], + "verifiedAt": "2026-10-28", + "notes": "The live page returned 403 on 2026-10-28; the address was read from its 2026-04-17 archive." + }, + { + "symbol": "FRAX", + "address": "0x853d955aCEf822Db058eb8505911ED77F175b99e", + "issuance": "native", + "issuer": "Frax", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve). Secondary reports say it was renamed \"Legacy Frax Dollar\" (FIP-428) after frxUSD launched; on-chain name is still \"Frax\"." + }, + { + "symbol": "frxETH", + "address": "0x5E8422345238F34275888049021821E8E08CAa1f", + "issuance": "native", + "issuer": "Frax", + "sources": [ + "https://docs.frax.finance/frax-ether/frxeth-and-sfrxeth-token-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Frax also has LayerZero frxETH on Ethereum at a different address." + }, + { + "symbol": "FXS", + "address": "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0", + "issuance": "native", + "issuer": "Frax", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "GHO", + "address": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "issuance": "native", + "issuer": "Aave", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/GhoEthereum.sol", + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "KNC", + "address": "0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202", + "issuance": "native", + "issuer": "Kyber Network", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "LBTC", + "address": "0x8236a87084f8B84306f72007F36F2618A5634494", + "issuance": "native", + "issuer": "Lombard", + "sources": [ + "https://docs.lombard.finance/learn/transparency/smart-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "LDO", + "address": "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + "issuance": "native", + "issuer": "Lido", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "LINK", + "address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", + "issuance": "native", + "issuer": "Chainlink", + "sources": [ + "https://docs.chain.link/resources/link-token-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "LUSD", + "address": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", + "issuance": "native", + "issuer": "Liquity (v1)", + "sources": [ + "https://docs.liquity.org/liquity-v1/documentation/resources" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "mETH", + "address": "0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa", + "issuance": "native", + "issuer": "Mantle", + "sources": [ + "https://docs.mantle.xyz/meth/components/smart-contracts/staking-meth" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "MKR", + "address": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + "issuance": "native", + "issuer": "Sky", + "legacy": true, + "sources": [ + "https://chainlog.sky.money/api/mainnet/active.json" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Superseded by SKY: the chainlog's MCD_GOV now points to SKY, and MKR converts one-way to SKY. symbol()/name() return bytes32." + }, + { + "symbol": "mUSD", + "address": "0xacA92E438df0B2401fF60dA7E4337B687a2435DA", + "issuance": "native", + "issuer": "MetaMask", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "osETH", + "address": "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38", + "issuance": "native", + "issuer": "StakeWise", + "sources": [ + "https://docs.stakewise.io/contracts/networks/Mainnet" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "PYUSD", + "address": "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8", + "issuance": "native", + "issuer": "Paxos (for PayPal)", + "sources": [ + "https://docs.paxos.com/guides/stablecoin/pyusd/mainnet" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "rETH", + "address": "0xae78736Cd615f374D3085123A210448E74Fc6393", + "issuance": "native", + "issuer": "Rocket Pool", + "sources": [ + "https://github.com/rocket-pool/docs.rocketpool.net/blob/main/docs/en/protocol/contracts-integrations.md" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "RLUSD", + "address": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD", + "issuance": "native", + "issuer": "Ripple", + "sources": [ + "https://docs.ripple.com/products/stablecoin/overview/token-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "RPL", + "address": "0xD33526068D116cE69F19A9ee46F0bd304F21A51f", + "issuance": "native", + "issuer": "Rocket Pool", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "rsETH", + "address": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + "issuance": "native", + "issuer": "Kelp", + "sources": [ + "https://github.com/Kelp-DAO/LRT-rsETH/blob/main/README.md" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "sDAI", + "address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "issuance": "native", + "issuer": "Sky (documented by Spark)", + "sources": [ + "https://docs.spark.finance/dev/savings/sdai-token" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "sfrxETH", + "address": "0xac3E018457B222d93114458476f3E3416Abbe38F", + "issuance": "native", + "issuer": "Frax", + "sources": [ + "https://docs.frax.finance/frax-ether/frxeth-and-sfrxeth-token-addresses" + ], + "verifiedAt": "2026-10-28", + "notes": "Frax also has LayerZero sfrxETH on Ethereum at a different address." + }, + { + "symbol": "SKY", + "address": "0x56072C95FAA701256059aa122697B133aDEd9279", + "issuance": "native", + "issuer": "Sky", + "sources": [ + "https://chainlog.sky.money/api/mainnet/active.json" + ], + "verifiedAt": "2026-10-28", + "notes": "Chainlog keys SKY and MCD_GOV." + }, + { + "symbol": "SNX", + "address": "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F", + "issuance": "native", + "issuer": "Synthetix", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "SolvBTC", + "address": "0x7A56E1C57C7475CCf742a1832B028F0456652F97", + "issuance": "native", + "issuer": "Solv", + "sources": [ + "https://github.com/solv-finance/SolvBTC/blob/main/deploy/SolvBTC/10099_export_SolvBTCInfos.js" + ], + "verifiedAt": "2026-10-28", + "notes": "docs.solv.finance returned 403; source is Solv's deploy script on GitHub." + }, + { + "symbol": "stETH", + "address": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "issuance": "native", + "issuer": "Lido", + "sources": [ + "https://docs.lido.fi/deployed-contracts/" + ], + "verifiedAt": "2026-10-28", + "notes": "Rebasing; see transferConstraints." + }, + { + "symbol": "STG", + "address": "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", + "issuance": "native", + "issuer": "Stargate", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "sUSDe", + "address": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", + "issuance": "native", + "issuer": "Ethena", + "sources": [ + "https://docs.ethena.fi/solution-design/key-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "sUSDS", + "address": "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD", + "issuance": "native", + "issuer": "Sky", + "sources": [ + "https://chainlog.sky.money/api/mainnet/active.json" + ], + "verifiedAt": "2026-10-28", + "notes": "Chainlog key SUSDS." + }, + { + "symbol": "syrupUSDT", + "address": "0x356B8d89c1e1239Cbbb9dE4815c39A1474d5BA7D", + "issuance": "native", + "issuer": "Maple", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "tBTC", + "address": "0x18084fbA666a33d37592fA2633fD49a74DD93a88", + "issuance": "native", + "issuer": "Threshold", + "sources": [ + "https://docs.threshold.network/contract-addresses/tbtc" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "tETH", + "address": "0xD11c452fc99cF405034ee446803b6F6c1F6d5ED8", + "issuance": "native", + "issuer": "Treehouse", + "sources": [ + "https://docs.treehouse.finance/protocol/tasset/key-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "UNI", + "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "issuance": "native", + "issuer": "Uniswap", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve)." + }, + { + "symbol": "USD1", + "address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", + "issuance": "native", + "issuer": "World Liberty Financial", + "sources": [ + "https://docs.worldlibertyfinancial.com/usd1-token/contract-addresses" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDC", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "issuance": "native", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/usdc-contract-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDe", + "address": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", + "issuance": "native", + "issuer": "Ethena", + "sources": [ + "https://docs.ethena.fi/solution-design/key-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDG", + "address": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", + "issuance": "native", + "issuer": "Paxos", + "sources": [ + "https://docs.paxos.com/guides/stablecoin/usdg/mainnet" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDS", + "address": "0xdC035D45d973E3EC169d2276DDab16f1e407384F", + "issuance": "native", + "issuer": "Sky", + "sources": [ + "https://chainlog.sky.money/api/mainnet/active.json" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Chainlog key USDS." + }, + { + "symbol": "USDT", + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "issuance": "native", + "issuer": "Tether", + "sources": [ + "https://tether.to/en/supported-protocols/" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDtb", + "address": "0xC139190F447e929f090Edeb554D95AbB8b18aC1C", + "issuance": "native", + "issuer": "Ethena (issued by Anchorage Digital Bank since October 2025)", + "sources": [ + "https://docs.usdtb.money/key-addresses" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "The current implementation has a blocklist and a pause switch; its whitelist roles are deprecated. A full-balance transfer to a fresh address simulated cleanly on 2026-10-28." + }, + { + "symbol": "WBTC", + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "issuance": "native", + "issuer": "BitGo", + "sources": [ + "https://wbtc.network/transparency" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "weETH", + "address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + "issuance": "native", + "issuer": "ether.fi", + "sources": [ + "https://etherfi.gitbook.io/etherfi/contracts-and-integrations/deployed-contracts" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "WETH", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "issuance": "native", + "issuer": "WETH9 (canonical wrapped Ether)", + "sources": [ + "https://ethereum.org/en/wrapped-eth/", + "https://developers.uniswap.org/docs/protocols/v3/deployments/v3-ethereum-deployments" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "wstETH", + "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "issuance": "native", + "issuer": "Lido", + "sources": [ + "https://docs.lido.fi/deployed-contracts/" + ], + "references": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "XAUt", + "address": "0x68749665FF8D2d112Fa859AA293F07A622782F38", + "issuance": "native", + "issuer": "Tether", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Source is Aave's address book (Core reserve). A full-balance transfer simulated cleanly on 2026-10-28; no fee." + }, + { + "symbol": "xSolvBTC", + "address": "0xd9D920AA40f578ab794426F5C90F6C731D159DEf", + "issuance": "native", + "issuer": "Solv", + "sources": [ + "https://github.com/solv-finance/SolvBTC/blob/main/deploy/xSolvBTCPool/1099_export_xSolvBTCPoolInfos.js" + ], + "references": [ + "https://docs.chain.link/ccip/directory/mainnet/token/xSolvBTC" + ], + "verifiedAt": "2026-10-28", + "notes": "Formerly SolvBTC.BBN (\"SolvBTC Babylon\"). The contract's name() and symbol() both return xSolvBTC; the catalog key was renamed to match." + } +] diff --git a/src/tokens/provenance/sepolia.json b/src/tokens/provenance/sepolia.json new file mode 100644 index 0000000..df5608c --- /dev/null +++ b/src/tokens/provenance/sepolia.json @@ -0,0 +1,66 @@ +[ + { + "symbol": "EURC", + "address": "0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4", + "issuance": "test", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/eurc-contract-addresses" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "LINK", + "address": "0x779877A7B0D9E8603169DdbD7836e478b4624789", + "issuance": "test", + "issuer": "Chainlink", + "sources": [ + "https://docs.chain.link/resources/link-token-contracts" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "UNI", + "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "issuance": "test", + "issuer": "Uniswap", + "sources": [], + "references": [ + "https://eth-sepolia.blockscout.com/address/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "https://eth.blockscout.com/address/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" + ], + "verifiedAt": "2026-10-28", + "notes": "Deployed by 0x4165…2Cfb, the same deployer as mainnet UNI, so the address matches mainnet." + }, + { + "symbol": "USDC", + "address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + "issuance": "test", + "issuer": "Circle", + "sources": [ + "https://developers.circle.com/stablecoins/usdc-contract-addresses" + ], + "verifiedAt": "2026-10-28" + }, + { + "symbol": "USDT", + "address": "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0", + "issuance": "test", + "issuer": "Aave V3 testnet faucet (not Tether)", + "sources": [ + "https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Sepolia.sol" + ], + "verifiedAt": "2026-10-28", + "notes": "Tether publishes no Sepolia USDT. This is Aave's faucet token; Aave's other Sepolia faucet tokens stay in Protocols.aaveV3.reserves." + }, + { + "symbol": "WETH", + "address": "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", + "issuance": "test", + "issuer": "WETH9 used by Uniswap", + "sources": [ + "https://developers.uniswap.org/docs/protocols/v3/deployments/v3-ethereum-deployments" + ], + "verifiedAt": "2026-10-28" + } +] diff --git a/src/tokens/types.ts b/src/tokens/types.ts index 1f79dc6..4546e3f 100644 --- a/src/tokens/types.ts +++ b/src/tokens/types.ts @@ -8,6 +8,11 @@ // - Identity — address, symbol, name, decimals // - Reference — explorer, website, optional links // - Display — logoUrl (always a URL, never a bundled binary) +// - Transfers — transferConstraints (behaviour a batch transfer must handle) +// +// Provenance (canonical source links, verification dates, native vs. +// bridged) lives in `src/tokens/provenance/*.json`. Tests read it; no +// module imports it, so the build does not ship it. // // Excluded by design: // - status / tags / research — vestigial from upstream tokenlists, @@ -45,6 +50,37 @@ export interface TokenLinks { readonly youtube?: string; } +/** + * Token behaviour that makes a plain `transfer(to, amount)` to an + * arbitrary address revert or deliver a different amount. Recorded so + * a consumer that batches transfers (a wallet sweep, a payout) can + * warn or validate before it builds the batch. + * + * Listing a token without constraints means only that none is known. + * It is not a promise that every transfer succeeds: issuer freezes, + * pauses and sanctions blocklists apply to most fiat-backed tokens and + * are deliberately not recorded here. Preparation should still + * simulate the actual transfers. See docs/TOKEN_CATALOG.md. + */ +export const TOKEN_TRANSFER_CONSTRAINT = Object.freeze({ + /** + * The recipient must be on the issuer's allowlist (KYC or qualified + * access). A transfer to an address the issuer has not onboarded + * reverts, including the holder's own new wallet. + */ + permissionedRecipient: "permissionedRecipient", + /** + * Balances are share-based and change without transfers. A transfer + * of the full balance can leave 1–2 wei behind, and the amount + * received can differ from the amount requested by rounding. + */ + rebasing: "rebasing", + /** The token takes a fee on transfer; the recipient receives less than the amount sent. */ + feeOnTransfer: "feeOnTransfer", +} as const); + +export type TokenTransferConstraint = (typeof TOKEN_TRANSFER_CONSTRAINT)[keyof typeof TOKEN_TRANSFER_CONSTRAINT]; + /** * Per-chain token deployment record. `address` and `decimals` are * required because they're the universal lookup primitives every @@ -69,6 +105,13 @@ export interface TokenChainEntry { readonly explorer?: string; readonly logoUrl?: string; readonly links?: TokenLinks; + /** + * Known transfer behaviour on this deployment. Absent when none is + * known. A recognized holding with constraints is still a legitimate + * holding: show it, and let transfer preparation decide whether a + * given transfer works. + */ + readonly transferConstraints?: readonly TokenTransferConstraint[]; } /** diff --git a/tests/token-coverage.test.ts b/tests/token-coverage.test.ts new file mode 100644 index 0000000..3e2f237 --- /dev/null +++ b/tests/token-coverage.test.ts @@ -0,0 +1,207 @@ +/** + * Token catalog coverage and provenance. + * + * Enforces the coverage target in scripts/token-catalog/coverage.ts and + * the provenance pins in src/tokens/provenance/*.json. Offline: on-chain + * reads are `yarn verify:tokens`. Policy: docs/TOKEN_CATALOG.md. + */ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { describe, it, expect } from "vitest"; + +import { Protocols, TOKEN_TRANSFER_CONSTRAINT, Tokens, type TokenChainEntry } from "../src"; +import { + CATALOG_CHAINS, + CHAIN_LABEL, + COVERAGE_CATEGORY, + EXPLICIT_BASELINE, + KNOWN_GAPS, + PROVENANCE_PENDING, + PROVENANCE_PENDING_AT_AUDIT, + aaveCoreReserves, +} from "../scripts/token-catalog/coverage"; +import { PROVENANCE_BY_CHAIN, TOKEN_ISSUANCE, provenanceFor } from "../scripts/token-catalog/provenance"; +import { renderCoverageReport } from "../scripts/token-coverage-report"; + +const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; +const ISSUANCE_VALUES = new Set(Object.values(TOKEN_ISSUANCE)); +const CONSTRAINT_VALUES = new Set(Object.values(TOKEN_TRANSFER_CONSTRAINT)); + +function catalogRows(chainId: number): Array<{ symbol: string; entry: TokenChainEntry }> { + const rows: Array<{ symbol: string; entry: TokenChainEntry }> = []; + for (const [symbol, byChain] of Object.entries(Tokens)) { + const entry = (byChain as Record)[chainId]; + if (entry) rows.push({ symbol, entry }); + } + return rows; +} + +const sameAddress = (left: string, right: string) => left.toLowerCase() === right.toLowerCase(); + +describe("coverage target", () => { + for (const chainId of CATALOG_CHAINS) { + const label = CHAIN_LABEL[chainId]; + const gaps = new Set(KNOWN_GAPS.filter((gap) => gap.chainId === chainId).map((gap) => gap.symbol)); + + it(`${label}: every baseline asset is listed or recorded as a known gap`, () => { + for (const { symbol } of EXPLICIT_BASELINE[chainId] ?? []) { + const listed = Tokens[symbol]?.[chainId as keyof (typeof Tokens)[string]] !== undefined; + expect(listed || gaps.has(symbol), `${label} ${symbol} is in the baseline but neither listed nor a known gap`).toBe(true); + } + }); + + it(`${label}: every non-expiring Aave V3 Core reserve is listed by address or recorded as a known gap`, () => { + const addresses = catalogRows(chainId).map(({ entry }) => entry.address); + for (const reserve of aaveCoreReserves(chainId)) { + const listed = addresses.some((address) => sameAddress(address, reserve.underlying)); + expect(listed || gaps.has(reserve.symbol), `${label} Aave reserve ${reserve.symbol} (${reserve.underlying})`).toBe(true); + } + }); + + it(`${label}: known gaps are not already listed`, () => { + for (const symbol of gaps) { + const listed = Tokens[symbol]?.[chainId as keyof (typeof Tokens)[string]] !== undefined; + expect(listed, `${label} ${symbol} is listed now; remove it from KNOWN_GAPS`).toBe(false); + } + }); + + it(`${label}: the native wrapper is the one Protocols.wrapped.weth names`, () => { + const wrapper = (EXPLICIT_BASELINE[chainId] ?? []).find((asset) => asset.category === COVERAGE_CATEGORY.nativeWrapper); + const expected = Protocols.wrapped.weth[chainId as keyof typeof Protocols.wrapped.weth]; + expect(wrapper, `${label} has no native wrapper in its baseline`).toBeDefined(); + expect(expected, `${label} has no Protocols.wrapped.weth entry`).toBeDefined(); + const entry = Tokens[wrapper!.symbol]?.[chainId as keyof (typeof Tokens)[string]]; + expect(entry && sameAddress(entry.address, expected!), `${label} ${wrapper!.symbol}`).toBe(true); + }); + } + + it("every known gap gives a reason", () => { + for (const gap of KNOWN_GAPS) expect(gap.reason.trim().length, `${gap.chainId} ${gap.symbol}`).toBeGreaterThan(0); + }); +}); + +describe("provenance", () => { + for (const [chainId, rows] of PROVENANCE_BY_CHAIN) { + const label = CHAIN_LABEL[chainId]; + const catalog = new Map(catalogRows(chainId).map(({ symbol, entry }) => [symbol, entry])); + + it(`${label}: each provenance row pins an address the catalog lists under that symbol`, () => { + const seen = new Set(); + for (const row of rows) { + expect(seen.has(row.symbol), `${label} ${row.symbol} has two provenance rows`).toBe(false); + seen.add(row.symbol); + const entry = catalog.get(row.symbol); + expect(entry, `${label} provenance for ${row.symbol}, which the catalog does not list`).toBeDefined(); + expect(entry && sameAddress(entry.address, row.address), `${label} ${row.symbol}: catalog ${entry?.address}, provenance ${row.address}`).toBe(true); + } + }); + + it(`${label}: provenance rows are complete`, () => { + for (const row of rows) { + const where = `${label} ${row.symbol}`; + expect(ISSUANCE_VALUES.has(row.issuance), `${where} issuance "${row.issuance}"`).toBe(true); + expect(row.issuer.trim().length, `${where} issuer`).toBeGreaterThan(0); + expect(row.verifiedAt, `${where} verifiedAt`).toMatch(ISO_DATE); + expect(row.sources.length + (row.references?.length ?? 0), `${where} cites no source`).toBeGreaterThan(0); + for (const url of [...row.sources, ...(row.references ?? [])]) expect(url, where).toMatch(/^https:\/\//); + if (row.issuance === TOKEN_ISSUANCE.bridged) expect(row.bridge?.trim().length, `${where} bridged without a bridge name`).toBeGreaterThan(0); + } + }); + + it(`${label}: baseline assets cite an issuer or protocol source`, () => { + const byChain = new Map(rows.map((row) => [row.symbol, row])); + for (const { symbol } of EXPLICIT_BASELINE[chainId] ?? []) { + if (!catalog.has(symbol)) continue; + expect(byChain.get(symbol)?.sources.length ?? 0, `${label} ${symbol} needs a canonical source`).toBeGreaterThan(0); + } + }); + + it(`${label}: every catalog row has provenance or is on the pre-audit pending list`, () => { + const pinned = new Set(rows.map((row) => row.symbol)); + const pending = new Set(PROVENANCE_PENDING[chainId] ?? []); + for (const symbol of catalog.keys()) { + expect(pinned.has(symbol) || pending.has(symbol), `${label} ${symbol} has no provenance row`).toBe(true); + } + expect(pending.size, `${label} PROVENANCE_PENDING grew; new rows need a provenance row instead`).toBeLessThanOrEqual( + PROVENANCE_PENDING_AT_AUDIT[chainId] ?? 0, + ); + for (const symbol of pending) { + expect(catalog.has(symbol), `${label} ${symbol} is pending but not listed; drop it from PROVENANCE_PENDING`).toBe(true); + expect(pinned.has(symbol), `${label} ${symbol} has provenance now; drop it from PROVENANCE_PENDING`).toBe(false); + } + }); + } + + it("covers every catalog chain", () => { + expect(PROVENANCE_BY_CHAIN.map(([chainId]) => chainId).sort()).toEqual([...CATALOG_CHAINS].sort()); + for (const chainId of CATALOG_CHAINS) expect(provenanceFor(chainId).length, CHAIN_LABEL[chainId]).toBeGreaterThan(0); + }); +}); + +describe("cross-registry consistency", () => { + it("one address means one symbol, on every chain", () => { + // lookupToken's cross-chain fallback returns the first match for an + // address. That is only safe while an address never names two assets. + const symbolByAddress = new Map(); + for (const [symbol, byChain] of Object.entries(Tokens)) { + for (const entry of Object.values(byChain)) { + if (!entry) continue; + const key = entry.address.toLowerCase(); + const existing = symbolByAddress.get(key); + expect(existing === undefined || existing === symbol, `${entry.address} is both ${existing} and ${symbol}`).toBe(true); + symbolByAddress.set(key, symbol); + } + } + }); + + it("protocol modules and the catalog agree on token addresses", () => { + // `null` symbol = the chain's native wrapper. Uniswap's `tokens.WETH` + // slot means "wrapped native" (WBNB on BNB Chain), like wrapped.weth. + const pairs: Array<[string, Partial>, string | null]> = [ + ["lido.steth", Protocols.lido.steth, "stETH"], + ["lido.wsteth", Protocols.lido.wsteth, "wstETH"], + ["ethena.usde", Protocols.ethena.usde, "USDe"], + ["ethena.susde", Protocols.ethena.susde, "sUSDe"], + ["rocketPool.reth", Protocols.rocketPool.reth, "rETH"], + ["sky.sdai", Protocols.sky.sdai, "sDAI"], + ["fraxEther.frxeth", Protocols.fraxEther.frxeth, "frxETH"], + ["fraxEther.sfrxeth", Protocols.fraxEther.sfrxeth, "sfrxETH"], + ["uniswapV3.tokens.USDC", Protocols.uniswapV3.tokens.USDC, "USDC"], + ["uniswapV3.tokens.WETH", Protocols.uniswapV3.tokens.WETH, null], + ["wrapped.weth", Protocols.wrapped.weth, null], + ]; + for (const [path, byChain, pairedSymbol] of pairs) { + for (const [chainId, address] of Object.entries(byChain)) { + if (!address || !CATALOG_CHAINS.includes(Number(chainId))) continue; + const symbol = + pairedSymbol ?? + (EXPLICIT_BASELINE[Number(chainId)] ?? []).find((asset) => asset.category === COVERAGE_CATEGORY.nativeWrapper)!.symbol; + const entry = Tokens[symbol]?.[Number(chainId) as keyof (typeof Tokens)[string]]; + expect(entry, `Protocols.${path}[${chainId}] is not in Tokens.${symbol}`).toBeDefined(); + expect(entry && sameAddress(entry.address, address), `Protocols.${path}[${chainId}] ${address} vs Tokens.${symbol} ${entry?.address}`).toBe(true); + } + } + }); +}); + +describe("transfer constraints", () => { + it("use known values, without repeats", () => { + for (const [symbol, byChain] of Object.entries(Tokens)) { + for (const [chainId, entry] of Object.entries(byChain)) { + const constraints = entry?.transferConstraints; + if (!constraints) continue; + expect(constraints.length, `${symbol}[${chainId}] has an empty list; omit the field`).toBeGreaterThan(0); + expect(new Set(constraints).size, `${symbol}[${chainId}] repeats a constraint`).toBe(constraints.length); + for (const constraint of constraints) expect(CONSTRAINT_VALUES.has(constraint), `${symbol}[${chainId}] "${constraint}"`).toBe(true); + } + } + }); +}); + +describe("docs/TOKEN_COVERAGE.md", () => { + it("matches the catalog (run `yarn report:tokens` after editing token data)", () => { + const committed = readFileSync(fileURLToPath(new URL("../docs/TOKEN_COVERAGE.md", import.meta.url)), "utf8"); + expect(committed).toBe(renderCoverageReport()); + }); +}); diff --git a/tests/tokens.test.ts b/tests/tokens.test.ts index e67d32f..5136684 100644 --- a/tests/tokens.test.ts +++ b/tests/tokens.test.ts @@ -6,8 +6,9 @@ * Reverse lookup (`lookupToken`) gets parallel coverage because it's * the access pattern aggregators and summarizers use. */ +import { getAddress } from "ethers"; import { describe, it, expect } from "vitest"; -import { Chains, Tokens, lookupToken } from "../src"; +import { Chains, Protocols, Tokens, lookupToken } from "../src"; const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/; const CHAIN_IDS = new Set(Object.values(Chains)); @@ -47,6 +48,18 @@ describe("Tokens namespace", () => { } }); + it("every address is EIP-55 checksummed", () => { + // Mixed-case addresses carry a checksum. A hand-typed one with the + // wrong casing passes the regex above but is rejected by ethers, + // viem and wallets. + for (const [symbol, byChain] of Object.entries(Tokens)) { + for (const [chainId, entry] of Object.entries(byChain)) { + if (!entry) continue; + expect(entry.address, `${symbol}[${chainId}]`).toBe(getAddress(entry.address.toLowerCase())); + } + } + }); + it("USDC on mainnet matches the address Uniswap V3 also ships", () => { // Sanity cross-check: the pre-existing per-protocol token map // and the top-level catalog must not drift. If this trips, decide @@ -115,6 +128,36 @@ describe("lookupToken()", () => { expect(result?.symbol).toBe("LINK"); }); + it("labels BNB Chain's WBNB as WBNB, not via Uniswap's wrapped-native WETH slot", () => { + // Protocols.uniswapV3.tokens.WETH[BnbMainnet] holds WBNB (the slot + // means "wrapped native"). Before WBNB was catalogued, the + // per-protocol fallback answered this lookup with symbol "WETH". + const result = lookupToken(Chains.BnbMainnet, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"); + expect(result?.symbol).toBe("WBNB"); + expect(result?.address).toBe(Protocols.wrapped.weth[Chains.BnbMainnet]); + }); + + it("resolves RLUSD on Ethereum and Base, which use different addresses", () => { + const ethereum = lookupToken(Chains.EthereumMainnet, "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD"); + const base = lookupToken(Chains.BaseMainnet, "0x8d58C0C60B8D6b88Fa98B291a646dB34d0F98258"); + expect(ethereum?.symbol).toBe("RLUSD"); + expect(base?.symbol).toBe("RLUSD"); + expect(ethereum?.decimals).toBe(18); + expect(base?.decimals).toBe(18); + }); + + it("names Solv's rebranded Ethereum token xSolvBTC, not SolvBTC.BBN", () => { + // name() and symbol() both return xSolvBTC since Solv's rebrand. + const result = lookupToken(Chains.EthereumMainnet, "0xd9D920AA40f578ab794426F5C90F6C731D159DEf"); + expect(result?.symbol).toBe("xSolvBTC"); + expect(Tokens["SolvBTC.BBN"]).toBeUndefined(); + }); + + it("carries the Base Sepolia USDT contract's 18 decimals", () => { + // The row said 6 until the 2026-10-28 on-chain check; decimals() returns 18. + expect(Tokens.USDT[Chains.BaseSepolia]?.decimals).toBe(18); + }); + it("distinguishes canonical Sepolia LINK from mainnet LINK", () => { // Tokens.LINK[Sepolia] is the canonical Sepolia ChainLink Token // (0x779877…4789), not the AAVE-V3 faucet variant. The faucet diff --git a/tests/verify-tokens.test.ts b/tests/verify-tokens.test.ts new file mode 100644 index 0000000..468e2e0 --- /dev/null +++ b/tests/verify-tokens.test.ts @@ -0,0 +1,71 @@ +/** + * `yarn verify:tokens` input handling. Offline: the on-chain checks need + * network access and are not run here. + * + * The property under test: a run that exits 0 checked every candidate + * row. A row the run would skip must fail it instead. + */ +import { describe, it, expect } from "vitest"; + +import { Chains } from "../src"; +import { parseArgs, parseCandidateRows } from "../scripts/verify-tokens"; + +const WBNB = { chainId: Chains.BnbMainnet, symbol: "WBNB", address: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", decimals: 18 }; +const RLUSD = { chainId: Chains.EthereumMainnet, symbol: "RLUSD", address: "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD", decimals: 18 }; + +describe("parseCandidateRows()", () => { + it("accepts well-formed rows on catalog chains", () => { + expect(parseCandidateRows([WBNB, RLUSD])).toEqual([WBNB, RLUSD]); + expect(parseCandidateRows([WBNB], Chains.BnbMainnet)).toEqual([WBNB]); + }); + + it("rejects a row on a chain without a catalog file, instead of skipping it", () => { + // Before: `0 rows checked, 0 with errors`, exit 0. + expect(() => parseCandidateRows([{ ...RLUSD, chainId: Chains.ArbitrumOne }])).toThrow(/chainId 42161 is not a catalog chain/); + }); + + it("rejects a row without a chainId", () => { + const { chainId: _omitted, ...withoutChain } = RLUSD; + expect(() => parseCandidateRows([withoutChain])).toThrow(/chainId undefined is not a catalog chain/); + }); + + it("rejects a row on another chain than --chain", () => { + expect(() => parseCandidateRows([WBNB, RLUSD], Chains.BnbMainnet)).toThrow(/row 1 \(RLUSD\): chainId 1, but --chain 56/); + }); + + it("reports every bad row in mixed input, and checks none", () => { + const input = [WBNB, { ...RLUSD, chainId: Chains.ArbitrumOne }, { ...RLUSD, chainId: Chains.OptimismMainnet }]; + expect(() => parseCandidateRows(input)).toThrow(/2 problem\(s\), nothing checked:\n {2}row 1 .*\n {2}row 2 /); + }); + + it("rejects malformed rows", () => { + expect(() => parseCandidateRows([{ ...WBNB, symbol: "" }])).toThrow(/symbol missing/); + expect(() => parseCandidateRows([{ ...WBNB, address: "0x1234" }])).toThrow(/is not an address/); + // Base RLUSD with the wrong checksum casing an earlier draft carried. + expect(() => parseCandidateRows([{ ...WBNB, address: "0x8d58C0C60B8D6b88Fa98B291a646Db34d0F98258" }])).toThrow(/bad EIP-55 checksum/); + expect(() => parseCandidateRows([{ ...WBNB, decimals: 6.5 }])).toThrow(/decimals 6.5/); + expect(() => parseCandidateRows([{ ...WBNB, decimals: "18" }])).toThrow(/decimals "18"/); + expect(() => parseCandidateRows([null])).toThrow(/row 0: not an object/); + }); + + it("rejects input that is not a non-empty array", () => { + expect(() => parseCandidateRows(WBNB)).toThrow(/expected a JSON array/); + expect(() => parseCandidateRows([])).toThrow(/the file has no rows/); + }); +}); + +describe("parseArgs()", () => { + it("parses the documented flags", () => { + expect(parseArgs([])).toEqual({ chain: undefined, json: false, rowsFile: undefined }); + expect(parseArgs(["--chain", "56", "--json", "--rows", "new.json"])).toEqual({ chain: 56, json: true, rowsFile: "new.json" }); + }); + + it("rejects --rows without a file, instead of checking the whole catalog", () => { + expect(() => parseArgs(["--rows"])).toThrow(/--rows needs a value/); + expect(() => parseArgs(["--rows", "--json"])).toThrow(/--rows needs a value/); + }); + + it("rejects an unknown flag, such as a mistyped --rows", () => { + expect(() => parseArgs(["--row", "new.json"])).toThrow(/unknown argument --row/); + }); +});