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.


Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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