Skip to content

feat(ingest): Confirmation depth and reorg handling for EVM deposits #222

Description

@Emmyt24

Depends on: #221. Blocks: #224.

Description

This is the highest-risk issue in the epic. Octo currently credits a deposit the moment it sees
it, because on Stellar that is correct: a transaction_successful payment is final and the ledger
does not reorganise. That assumption is wired into the design — see the guarantees at the top of
crates/ingest/src/lib.rs and
crates/store/src/lib.rs.

On EVM, blocks reorg. A deposit seen at block N can vanish. If Octo credits on sight, an attacker
can deposit, get credited, force or exploit a reorg, and withdraw against a balance that no longer
exists. Even absent an attacker, ordinary 1–2 block reorgs happen routinely on L1 and L2s.

Introduce a confirmation state machine and a reorg detector that can reverse a credit.

Requirements and context

  • Deposits move through explicit states: detectedconfirmingconfirmed → creditable.
    Only confirmed funds are spendable. A separate orphaned terminal state records reversals for
    audit — never delete the row; the ledger is append-only and reversals must be visible.
  • Confirmation depth is per chain, configured in feat(config): Per-chain configuration and runtime chain registry #216. Ethereum L1 and an L2 with a centralised
    sequencer have very different risk profiles, and L2s can have deep reorgs on sequencer
    failover. Do not hard-code a number.
  • Reorg detection: store the block hash alongside the block number for each processed block, and
    on each poll verify the parent hash still matches. A number-only cursor cannot detect a reorg —
    the same height with a different hash looks identical.
  • On reorg: rewind the cursor to the last common ancestor, mark affected deposits orphaned, emit a
    reversal webhook, and re-scan. Rewinding must be bounded — an unbounded rewind on a malicious RPC
    is a DoS.
  • Security: the window between crediting and finality is the exploitable window. State clearly
    in the threat model what depth is used per chain and what that implies. Consider whether an
    orphaned deposit that was already withdrawn against is possible, and what the system does about
    it — an honest "this is prevented by requiring N confirmations before spendability" is the
    expected answer.
  • Consider finalized / safe block tags (post-Merge) as a stronger signal than depth counting
    where the provider supports them.

Suggested execution

Branch: feat/evm-confirmation-and-reorg

Implement changes

  • Migration 00NN_deposit_confirmations.sql: add confirmation_state, block_number,
    block_hash, confirmations, and orphaned_at to transactions; add a partial index over
    rows still confirming, since that is the hot query.
  • Add a confirmation tracker that re-checks confirming deposits each tick, promotes them at depth,
    and emits deposit.confirmed.
  • Add reorg detection via parent-hash chaining, with a bounded rewind depth (configurable, default
    ≥ 2× confirmation depth) and a loud alert if the bound is hit.
  • Add reversal: mark orphaned, adjust balances, emit deposit.orphaned via
    octo-webhooks.
  • Ensure balance queries only sum confirmed rows. Audit every existing balance/aggregate query
    for this — one missed query makes unconfirmed funds spendable and defeats the whole issue.

Test and commit

  • Reorg integration test using Anvil snapshot/revert (chore(testing): Anvil-based EVM integration test harness #219): deposit at block N, confirm it, force
    a reorg, assert the deposit is marked orphaned, the balance is reduced, and the webhook fires.
    This is the acceptance test for the issue.
  • Test that a deposit below confirmation depth is not spendable — attempt a withdrawal against
    it and assert rejection.
  • Test progressive confirmation counting and promotion at exactly the configured depth.
  • Test deep-reorg bounding: a reorg deeper than the rewind bound alerts rather than looping.
  • Test that re-scanning after a reorg re-detects a transaction that survived, without
    double-crediting.
  • Test that Stellar deposits are unaffected and still credit immediately.
  • Update docs/threat-model.md and docs/deposit-model.md
    with the per-chain depths and the reasoning behind each.

Example commit message

feat(ingest): confirmation depth and reorg handling for EVM

Stellar finality is instant, so Octo credits on sight. EVM blocks
reorg, so crediting on sight would let an attacker deposit, withdraw,
and reorg away the deposit.

Deposits now progress detected → confirming → confirmed, with only
confirmed rows spendable, and reorgs are detected by parent-hash
chaining (a number-only cursor cannot see a same-height different-hash
reorg). Affected deposits are marked orphaned and never deleted.

Refs #222

Guidelines

Security-critical. Requires two maintainer reviewers. Do not attempt as a first contribution. The
PR description must include the reorg test output and a per-chain depth justification.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaignarea/backendBackend crates: api, store, ingest, webhooks, bin/serverdifficulty/hardHard, complex, cross-cuttingrisk/security-criticalFund-loss or fund-theft risk if implemented incorrectlystatus/needs-maintainer-reviewRequires a maintainer as co-reviewer before mergetype/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