Skip to content

feat(wallet): EVM deposit sweep engine #224

Description

@Emmyt24

Depends on: #220, #225, #226. Blocks: nothing (terminal).

Description

Because EVM deposits land at N distinct per-customer addresses (#220) rather than in one muxed base
account, funds must be consolidated into a treasury address. This has no Stellar analogue at
all — the muxed model exists precisely so that sweeping is unnecessary
(docs/deposit-model.md).

Sweeping is genuinely hard and this issue holds real customer funds:

  • A deposit address holds ERC-20 tokens but no native ETH, so it cannot pay gas for its own
    transfer. The sweeper must send gas first, then sweep — a two-transaction dance where a crash
    between the two must be recoverable.
  • Gas cost can exceed the deposit value. Sweeping a $2 deposit for $4 of gas destroys value.
  • Sweeps must be idempotent. A double-sweep wastes gas; a lost sweep record strands funds.
  • The sweeper must hold spending keys for every deposit address, which is the largest departure
    from Octo's non-custodial posture in the entire epic (AD-4).

Requirements and context

  • Only sweep confirmed deposits (per feat(ingest): Confirmation depth and reorg handling for EVM deposits #222). Sweeping an unconfirmed deposit that then reorgs
    means paying gas to move money that never existed.
  • Economic gating is required, not optional: do not sweep when
    estimated_gas_cost > sweep_value × threshold. Accumulate and batch instead. Make the threshold
    configurable per chain — it is completely different on L1 versus Base.
  • Every sweep must be idempotent and crash-recoverable. Persist intent before broadcasting, and
    reconcile on restart. Reuse the idempotency-key pattern from
    Store::create_withdrawal.
  • Reuse feat(api): Nonce management, gas pricing, and EVM transaction lifecycle #226's nonce management. Do not build a second nonce allocator.
  • Security — state these plainly in the threat model:
    • The sweeper's key material can move all unswept customer funds. Bound the exposure: what is the
      maximum unswept balance at any time, and what is the blast radius of a sweeper compromise?
    • Non-hardened derivation (feat(store): EVM per-customer deposit addresses via HD derivation #220) means a leaked deposit key plus a leaked xpub compromises all
      siblings — so the sweeper's key store and the xpub must not share a compromise boundary.
    • Gas-funding transfers to deposit addresses are visible on-chain and fingerprint Octo's entire
      deposit address set
      to any observer. Note this as an accepted privacy trade-off, or mitigate.
    • Keys must be sealed with octo-crypto at rest and only opened
      inside the signing boundary, zeroized after.

Suggested execution

Branch: feat/evm-sweep-engine

Implement changes

Test and commit

  • Anvil tests (chore(testing): Anvil-based EVM integration test harness #219): fund a deposit address with a mock ERC-20, run the sweeper, assert the treasury
    balance increases by exactly the deposit amount and the sweep row reaches confirmed.
  • Crash-recovery test: kill the process between gas funding and sweep, restart, assert exactly
    one sweep occurs and no gas is double-sent.
  • Economic-gating test: a dust deposit whose gas exceeds its value is not swept.
  • Idempotency test: running the sweeper twice concurrently over the same deposit produces one sweep.
  • Test that an unconfirmed deposit is never swept.
  • A failure-path test: the sweep transaction reverts, and the sweep row lands in failed with the
    funds still safely at the deposit address.
  • Update docs/threat-model.md with the full sweeper key-exposure analysis.

Example commit message

feat(wallet): EVM deposit sweep engine

Per-customer EOAs (#220) mean funds must be consolidated into a treasury,
which the Stellar muxed model never required.

Sweeps are gas-funded then executed as a two-step, persisted-intent
state machine that reconciles against on-chain state on restart, so a
crash between funding and sweeping cannot double-send or strand funds.
Dust below the configurable gas-to-value threshold accumulates instead
of being swept at a loss.

Only confirmed deposits are swept — sweeping a reorg-able deposit
spends gas moving money that never existed.

Refs #224

Guidelines

Security-critical, holds customer funds. Two maintainer reviewers, and a written threat-model
section is part of the deliverable, not a follow-up.


Metadata

Metadata

Assignees

No one assigned

    Labels

    area/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