Skip to content

feat(store): ERC-20 token registry #223

Description

@Emmyt24

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.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).
  • On registration, call decimals() / symbol() via feat(evm-rpc): Resilient JSON-RPC client for EVM chains #218 and reject a mismatch against the
    submitted values rather than silently trusting either side.
  • Replace both USDC_TESTNET_ISSUER constants with registry lookups.
  • Add read-only API endpoints so clients can discover supported assets per chain.

Test and commit

  • Test that registration rejects a decimals mismatch against the on-chain value.
  • Test that a disabled token is not creditable and that an unregistered token is quarantined rather
    than credited or dropped.
  • Test address normalisation: registering 0xABC... and looking up 0xabc... resolves.
  • Test that two different contracts both reporting symbol() == "USDC" are distinct registry
    entries
    , and only the registered one is creditable.
  • Test that the Stellar asset path still resolves the same USDC issuer it did via the constant —
    proving the constant removal is behaviour-preserving.
  • Document the registry and the known-bad token classes in docs/architecture.md.

Example commit message

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/backendBackend crates: api, store, ingest, webhooks, bin/serverdifficulty/mediumMedium difficultygood first issueGood for newcomerstype/epicTracking issue for a multi-issue epic

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions