You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Stellar, a wallet's acceptable assets are knowable on-chain: a trustline is an explicit opt-in,
which is what crates/api/src/routes/trustlines.rs
exposes. ERC-20 has no equivalent — any address can receive any token, unsolicited, including
tokens designed to look like real ones.
There is also an existing wart worth fixing here: the testnet USDC issuer is a hard-coded constant
duplicated in two crates, with a comment acknowledging the duplication — see USDC_TESTNET_ISSUER in crates/ingest/src/lib.rs and crates/api/src/routes/payment_links.rs. A registry
subsumes both.
Build a per-chain token registry that is the single authority on what Octo will credit.
Requirements and context
Registry rows are keyed by CAIP-19 asset id (AD-1) and carry chain_id, contract address,
symbol, decimals, and enabled.
decimals must be verified on-chain at registration by calling decimals(), not taken from
operator input. USDC is 6 and DAI is 18; a wrong value here misprices every deposit of that token
by a factor of 10^12.
Unregistered tokens are never credited. They may be recorded for visibility (quarantine),
matching how unattributable Stellar deposits are handled today.
Registration is an admin-only operation. A user-registerable registry reintroduces exactly the
attack it exists to prevent.
Document the known-bad token classes explicitly, since operators will ask: fee-on-transfer
(received ≠ event value), rebasing (balance changes without a transfer), and tokens with an
upgradeable proxy or a blacklist/pause function that can freeze Octo's own treasury.
Security: an attacker deploying a contract with symbol() == "USDC" costs nothing. Symbol is
a display string with no uniqueness guarantee — match on contract address only, never symbol.
Suggested execution
Branch: feat/erc20-token-registry
Implement changes
Migration 00NN_token_registry.sql: a tokens table keyed by CAIP-19 with the fields above, UNIQUE (chain_id, contract_address) on the lowercase-normalised address, and rows seeded for
USDC on the target chains plus the existing Stellar assets so the hard-coded constants can be
deleted.
Store methods: register_token (admin), list_tokens(chain_id), get_token(caip19), is_creditable(chain_id, contract_address).
feat(store): per-chain ERC-20 token registry
ERC-20 has no trustline equivalent, so any address can receive any
token; the registry becomes the single authority on what Octo credits.
decimals() is read on-chain at registration rather than trusted from
input — USDC is 6 and DAI is 18, and a wrong value misprices deposits
by 10^12. Matching is on contract address only, never symbol, which
any attacker can spoof for free.
Also removes the duplicated USDC_TESTNET_ISSUER constants in
octo-ingest and octo-api.
Refs #223
Depends on: #214, #215. Blocks: #221, #225.
Description
On Stellar, a wallet's acceptable assets are knowable on-chain: a trustline is an explicit opt-in,
which is what
crates/api/src/routes/trustlines.rsexposes. ERC-20 has no equivalent — any address can receive any token, unsolicited, including
tokens designed to look like real ones.
There is also an existing wart worth fixing here: the testnet USDC issuer is a hard-coded constant
duplicated in two crates, with a comment acknowledging the duplication — see
USDC_TESTNET_ISSUERincrates/ingest/src/lib.rsandcrates/api/src/routes/payment_links.rs. A registrysubsumes both.
Build a per-chain token registry that is the single authority on what Octo will credit.
Requirements and context
chain_id, contract address,symbol,
decimals, andenabled.decimalsmust be verified on-chain at registration by callingdecimals(), not taken fromoperator input. USDC is 6 and DAI is 18; a wrong value here misprices every deposit of that token
by a factor of 10^12.
matching how unattributable Stellar deposits are handled today.
attack it exists to prevent.
(received ≠ event value), rebasing (balance changes without a transfer), and tokens with an
upgradeable proxy or a blacklist/pause function that can freeze Octo's own treasury.
symbol() == "USDC"costs nothing. Symbol isa display string with no uniqueness guarantee — match on contract address only, never symbol.
Suggested execution
Branch:
feat/erc20-token-registryImplement changes
00NN_token_registry.sql: atokenstable keyed by CAIP-19 with the fields above,UNIQUE (chain_id, contract_address)on the lowercase-normalised address, and rows seeded forUSDC on the target chains plus the existing Stellar assets so the hard-coded constants can be
deleted.
register_token(admin),list_tokens(chain_id),get_token(caip19),is_creditable(chain_id, contract_address).decimals()/symbol()via feat(evm-rpc): Resilient JSON-RPC client for EVM chains #218 and reject a mismatch against thesubmitted values rather than silently trusting either side.
USDC_TESTNET_ISSUERconstants with registry lookups.Test and commit
decimalsmismatch against the on-chain value.than credited or dropped.
0xABC...and looking up0xabc...resolves.symbol() == "USDC"are distinct registryentries, and only the registered one is creditable.
proving the constant removal is behaviour-preserving.
docs/architecture.md.Example commit message