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
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: detected → confirming → confirmed → 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.
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.
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_successfulpayment is final and the ledgerdoes not reorganise. That assumption is wired into the design — see the guarantees at the top of
crates/ingest/src/lib.rsandcrates/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
detected→confirming→confirmed→ creditable.Only
confirmedfunds are spendable. A separateorphanedterminal state records reversals foraudit — never delete the row; the ledger is append-only and reversals must be visible.
sequencer have very different risk profiles, and L2s can have deep reorgs on sequencer
failover. Do not hard-code a number.
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.
orphaned, emit areversal webhook, and re-scan. Rewinding must be bounded — an unbounded rewind on a malicious RPC
is a DoS.
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.
finalized/safeblock tags (post-Merge) as a stronger signal than depth countingwhere the provider supports them.
Suggested execution
Branch:
feat/evm-confirmation-and-reorgImplement changes
00NN_deposit_confirmations.sql: addconfirmation_state,block_number,block_hash,confirmations, andorphaned_attotransactions; add a partial index overrows still confirming, since that is the hot query.
and emits
deposit.confirmed.≥ 2× confirmation depth) and a loud alert if the bound is hit.
orphaned, adjust balances, emitdeposit.orphanedviaocto-webhooks.confirmedrows. Audit every existing balance/aggregate queryfor this — one missed query makes unconfirmed funds spendable and defeats the whole issue.
Test and commit
a reorg, assert the deposit is marked
orphaned, the balance is reduced, and the webhook fires.This is the acceptance test for the issue.
it and assert rejection.
double-crediting.
docs/threat-model.mdanddocs/deposit-model.mdwith the per-chain depths and the reasoning behind each.
Example commit message
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.