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
octo-ingest polls Horizon /payments oldest-first from a saved paging_token, attributes each payment by muxed id or memo id, and records it idempotently, with
dedup on the Horizon TOID. EVM deposit detection is structurally different:
No per-account payments feed. You scan eth_getLogs for ERC-20 Transfer events whose to topic matches one of your deposit addresses.
The cursor is a block number, not an opaque token.
Native ETH transfers emit no logs at all — they are only visible by inspecting block
transactions or debug_traceBlock. Decide and document whether native ETH deposits are in scope
(recommendation: out of scope for v1, ERC-20 stablecoins only, stated explicitly in the docs
rather than silently unsupported).
Non-standard ERC-20s exist: fee-on-transfer tokens where the amount received ≠ the amount in the
event, and rebasing tokens. The registry (feat(store): ERC-20 token registry #223) is the defence; this worker must trust the
registry, not the token.
Build the EVM ingest worker to the same reliability bar as the Stellar one.
Requirements and context
Reuse the existing supervisor and backoff design.wallets_due_for_poll
(crates/store/src/lib.rs) already implements activity-based poll
tiers (active/idle/dormant); do not build a parallel scheduler.
Crash safety: the cursor advances only after a record is durably processed, matching the
guarantee documented in crates/ingest/src/lib.rs. A crash mid-batch
must resume without missing or double-processing.
Preserve the quarantine behaviour: a transfer to a known address for an unregistered token is
recorded without attribution rather than guessed or dropped.
Security: verify the log's address field is the registered token contract. Anyone can
deploy a contract emitting a fake Transfer event with any topics; attributing on topics alone
lets an attacker mint balances for free. This is the single most important check in this issue.
Suggested execution
Branch: feat/evm-ingest-worker
Implement changes
Add crates/ingest/src/evm.rs with an EvmIngestor mirroring Ingestor's shape and returning
the same Processed { Recorded, Duplicate, Skipped } enum.
Implement log scanning: eth_getLogs filtered by registered token addresses and the Transfer(address,address,uint256) topic0, with the to topic matched against deposit addresses.
Persist a block-number cursor per (wallet, chain), reusing the ingest_cursor table with the
chain-scoped columns from feat(store): Multi-chain database schema migration #214, and keep mark_polled's "looked at" vs "saw activity" distinction.
Adversarial test — the critical one: deploy a hostile contract that emits a Transfer event with a deposit address in the to topic and a huge value, and assert it is not credited because the emitting contract is not a registered token. Model on crates/ingest/tests/adversarial_replay_tests.rs.
Replay/idempotency test: processing the same log range twice records nothing new.
feat(ingest): EVM deposit detection via ERC-20 Transfer logs
Scans eth_getLogs for registered token contracts with a block-number
cursor, advancing only after durable processing so a crash resumes
exactly-once — the same guarantee the Horizon path gives.
Logs are matched on the emitting contract address, not topics alone:
any contract can emit a Transfer event with arbitrary topics, so
topic-only attribution would let an attacker mint balances.
Deposits are recorded unconfirmed; crediting is gated on #222.
Refs #221
Guidelines
Large issue — split into stacked PRs (cursor/scanning, then decoding/attribution) if that helps
review. Do not merge a version that credits balances before #222 lands.
Depends on: #218, #220, #223. Blocks: #222.
Description
octo-ingestpolls Horizon/paymentsoldest-first from a savedpaging_token, attributes each payment by muxed id or memo id, and records it idempotently, withdedup on the Horizon TOID. EVM deposit detection is structurally different:
eth_getLogsfor ERC-20Transferevents whosetotopic matches one of your deposit addresses.transactions or
debug_traceBlock. Decide and document whether native ETH deposits are in scope(recommendation: out of scope for v1, ERC-20 stablecoins only, stated explicitly in the docs
rather than silently unsupported).
event, and rebasing tokens. The registry (feat(store): ERC-20 token registry #223) is the defence; this worker must trust the
registry, not the token.
Build the EVM ingest worker to the same reliability bar as the Stellar one.
Requirements and context
wallets_due_for_poll(
crates/store/src/lib.rs) already implements activity-based polltiers (active/idle/dormant); do not build a parallel scheduler.
guarantee documented in
crates/ingest/src/lib.rs. A crash mid-batchmust resume without missing or double-processing.
RangeTooLargefrom feat(evm-rpc): Resilient JSON-RPC client for EVM chains #218, halve the block range and retry. A fixedrange will fail on some providers and waste requests on others.
is gated by feat(ingest): Confirmation depth and reorg handling for EVM deposits #222. Merging feat(ingest): EVM ingest worker — ERC-20 Transfer log scanning #221 without feat(ingest): Confirmation depth and reorg handling for EVM deposits #222 must not create spendable balances — enforce that in code,
not by convention.
recorded without attribution rather than guessed or dropped.
addressfield is the registered token contract. Anyone candeploy a contract emitting a fake
Transferevent with any topics; attributing on topics alonelets an attacker mint balances for free. This is the single most important check in this issue.
Suggested execution
Branch:
feat/evm-ingest-workerImplement changes
crates/ingest/src/evm.rswith anEvmIngestormirroringIngestor's shape and returningthe same
Processed { Recorded, Duplicate, Skipped }enum.eth_getLogsfiltered by registered token addresses and theTransfer(address,address,uint256)topic0, with thetotopic matched against deposit addresses.Transfercorrectly:from/toare indexed (topics 1 and 2, left-padded to 32 bytes)and
valueis in data. ParsevalueasU256via feat(store): Arbitrary-precision amounts — replace i64 stroops with NUMERIC(78,0) #215'sAmount.ingest_cursortable with thechain-scoped columns from feat(store): Multi-chain database schema migration #214, and keep
mark_polled's "looked at" vs "saw activity" distinction.(chain_id, tx_hash, log_index)via the index from feat(store): Multi-chain database schema migration #214.Test and commit
and recorded with the exact amount at 6 and 18 decimals.
Transferevent with a deposit address in thetotopic and a hugevalue, and assert it isnot credited because the emitting contract is not a registered token. Model on
crates/ingest/tests/adversarial_replay_tests.rs.resume_replay_tests.rs.RangeTooLargecauses bisection, not a stall.docs/ingest-integration.md.Example commit message
Guidelines
Large issue — split into stacked PRs (cursor/scanning, then decoding/attribution) if that helps
review. Do not merge a version that credits balances before #222 lands.