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
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).
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.
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?
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
Migration 00NN_sweeps.sql: a sweeps table recording source address, token, amount, gas-funding
tx hash, sweep tx hash, state machine (pending → gas_funded → submitted → confirmed / failed), and an idempotency key.
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.
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:
transfer. The sweeper must send gas first, then sweep — a two-transaction dance where a crash
between the two must be recoverable.
from Octo's non-custodial posture in the entire epic (AD-4).
Requirements and context
confirmeddeposits (per feat(ingest): Confirmation depth and reorg handling for EVM deposits #222). Sweeping an unconfirmed deposit that then reorgsmeans paying gas to move money that never existed.
estimated_gas_cost > sweep_value × threshold. Accumulate and batch instead. Make the thresholdconfigurable per chain — it is completely different on L1 versus Base.
reconcile on restart. Reuse the idempotency-key pattern from
Store::create_withdrawal.maximum unswept balance at any time, and what is the blast radius of a sweeper compromise?
siblings — so the sweeper's key store and the xpub must not share a compromise boundary.
deposit address set to any observer. Note this as an accepted privacy trade-off, or mitigate.
octo-cryptoat rest and only openedinside the signing boundary, zeroized after.
Suggested execution
Branch:
feat/evm-sweep-engineImplement changes
00NN_sweeps.sql: asweepstable recording source address, token, amount, gas-fundingtx hash, sweep tx hash, state machine (
pending→gas_funded→submitted→confirmed/failed), and an idempotency key.build and sign the ERC-20
transferto treasury, submit via feat(api): EVM signed-transaction relay with policy validation #225/feat(api): Nonce management, gas pricing, and EVM transaction lifecycle #226, and track to confirmation.before initiating anything new.
gate, sweeps stuck in a non-terminal state.
gas-funding step.
Test and commit
balance increases by exactly the deposit amount and the sweep row reaches
confirmed.one sweep occurs and no gas is double-sent.
failedwith thefunds still safely at the deposit address.
docs/threat-model.mdwith the full sweeper key-exposure analysis.Example commit message
Guidelines
Security-critical, holds customer funds. Two maintainer reviewers, and a written threat-model
section is part of the deliverable, not a follow-up.