Skip to content

docs: design for supporting deposits from CEX (DEFI-2096)#10652

Open
gregorydemay wants to merge 41 commits into
masterfrom
ic_DEFI-2096_design-support-deposit-from-cex
Open

docs: design for supporting deposits from CEX (DEFI-2096)#10652
gregorydemay wants to merge 41 commits into
masterfrom
ic_DEFI-2096_design-support-deposit-from-cex

Conversation

@gregorydemay

@gregorydemay gregorydemay commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Design doc for DEFI-2096: allow onramping ckUSDC/ckUSDT (and generally any ckERC20, later ckETH) directly from a centralized exchange withdrawal.

Deposits currently require calling the helper smart contract, which a CEX cannot do: a CEX withdrawal is a plain transfer from a shared hot wallet, carrying no IC principal. The proposed design gives each IC account a unique tECDSA-derived deposit address and sweeps funds to the minter using EIP-7702 (Pectra), so deposit addresses never need ETH for gas and remain key-recoverable independently of any contract code.

The doc covers the requirements, claim-based deposit detection, the sweeper delegate, fee model, a phased delivery (ckERC20 first, then ckETH with its balance-based detection and fixed-21k-gas constraints), and the discussed alternatives (CREATE2 forwarders, ERC-4337, permit-based sponsoring).

The runnable proof-of-concept the doc references lives in a stacked follow-up, #10670.

🤖 Generated with Claude Code

Design proposal for onramping ckERC20 (and later ckETH) directly from a
centralized exchange withdrawal: per-account tECDSA deposit addresses
swept via EIP-7702, so that depositors never need ETH for gas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a design document describing how to support ckERC20 (and later ckETH) deposits directly from centralized exchanges by assigning each IC account a deterministic tECDSA-derived deposit address and sweeping funds to the minter using EIP-7702.

Changes:

  • Introduces a new design doc covering motivation, requirements, and phased rollout (ckERC20 first, then ckETH).
  • Specifies claim-based deposit detection, sweeping/delegation flow, fee model, and a test plan.
  • Documents considered alternatives (CREATE2 forwarders, ERC-4337, permit-based approaches) and rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rs/ethereum/cketh/docs/deposit_from_cex.md Outdated
gregorydemay and others added 4 commits July 3, 2026 14:05
Foundry-based script (local anvil, Prague hardfork) demonstrating the
core mechanism: an unfunded minter-derived deposit EOA receives a plain
USDT-style ERC-20 transfer and is swept to the minter in a single
type-0x04 transaction, with all gas paid by the minter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the bash/cast demo with a standalone cargo binary that prints
the full sweep transaction details and asserts gas usage, and adds a
batched sweep: one type-0x04 transaction targeting several deposit EOAs
via a batch entry point on the CkSweeper delegate (measured ~26k
marginal gas per additional EOA vs ~67k for a standalone sweep). The
design doc is updated accordingly (batching no longer needs Multicall3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anvil's default hardfork is the latest supported one, which includes
EIP-7702 in any release since Pectra; reproducibility comes from
pinning the foundry image version, not from naming the fork.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread rs/ethereum/cketh/docs/deposit_from_cex_demo/src/main.rs Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex_demo/src/main.rs Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex_demo/src/main.rs Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex_demo/src/main.rs Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex_demo/src/main.rs Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex_demo/src/main.rs Outdated
gregorydemay and others added 8 commits July 3, 2026 15:05
- separate wallets for the minter and the CEX (unrelated parties)
- rename *_PK constants to *_PRIVATE_KEY
- print the raw signed transaction hex of each sweep
- print the minter's nonce for every transaction it signs
- hard-code the expected gas used by both sweep transactions
- fix the EthBalance field reference in the design doc

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esign

The sweeper delegate can call the existing helper contract
(DepositHelperWithSubaccount.sol) instead of transferring directly: the
sweep then emits the canonical ReceivedEthOrErc20 event, so the minter's
existing crediting pipeline is reused unchanged and deposit detection is
demoted to a sweep-scheduling hint. Since the principal becomes a sweep
argument, sweeping is restricted to the minter. Sweeps can be scheduled
on deposits observed at the latest block without waiting for finality:
crediting only follows the finalized helper event, so a reorg merely
wastes gas. The decision between direct sweep (A) and sweep-through-
helper (B) is left open in the design doc.

The demo exercises both variants against the real helper bytecode,
asserts the emitted events carry the right principals, and shows a
non-minter sweep attempt being rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registering a deposit address must trigger no tECDSA signature and no
Ethereum transaction: registrations are free for callers, so any eager
per-address spending would let an attacker drain the minter's cycles
and ETH. Delegation is signed and the sweep submitted only after a
balance of a supported token >= the per-token minimum has been observed
at the registered address, and balance scanning itself stays
claim-driven/bounded since the registered set is attacker-inflatable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observing balances of many registered deposit addresses (the R13 gate)
must not cost one HTTPS outcall per address and provider. Record the
dependency on the EVM-RPC canister eth_batch endpoint
(dfinity/evm-rpc-canister#561, in progress) and the Multicall3
aggregate3 alternative usable meanwhile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A native-ETH deposit carries its own gas, so a dedicated ETH deposit
address (second derivation schema tag) never needs EIP-7702 at all: the
minter sweeps it with a plain 21k-gas transfer signed by the address'
own derived key, paying gas from the swept balance. The address never
carries code, so fixed-21000-gas CEX withdrawals always succeed and R12
holds trivially. The set-and-clear delegation lifecycle on a shared
address is demoted to a fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep transactions are paid in ETH by the minter, but that ETH backs
ckETH 1:1. New requirement R14: burn-first from the minter's fee
account on the ckETH ledger, at least the transaction's maximum fee;
track burned-but-unspent as prepaid credit for subsequent burns, never
re-mint. Deposit fees are minted to a per-token fee account (full
deposited amount minted, supply stays equal to backing); converting
that per-token revenue into ckETH to replenish the fee account is a
treasury operation out of scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mermaid sequence diagrams for the full flow: ckUSDT under variant A
(direct sweep, mint on finalized deposit) and variant B (sweep through
the helper, existing pipeline mints), and ckETH in Phase 2 (dedicated
never-delegated address, deposit pays its own sweep gas). Diagramming
surfaced a gap now closed: ETH sweeps need no R14 burn, but under
variant A the sweep's max fee must be capped by the charged deposit fee
since crediting happens before the sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mermaid treats ';' as a statement separator, so semicolons inside note
text split the note into an invalid statement. All three diagrams now
validated with mermaid-cli.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread rs/ethereum/cketh/docs/deposit_from_cex.md Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex.md Outdated
Comment thread rs/ethereum/cketh/docs/deposit_from_cex.md Outdated
gregorydemay and others added 4 commits July 6, 2026 07:37
Design section now walks the five flow steps (retrieve address, CEX
withdrawal, detection, crediting, sweeping, fee payment), each with a
pro/con table where variants exist. Addresses review comments:

- rename get_deposit_address to retrieve_deposit_address and explain
  why it must be an update call (registers and arms scanning)
- single-step UX (new R15): registration arms a per-address scanning
  window that a background task scans in bounded batches - no second
  canister call needed; notify_deposit demoted to optional accelerator
- detail blocklist screening for deposits at user-derived addresses:
  screen the Transfer log's from address against src/blocklist.rs; if
  blocked, no mint and the address is excluded from sweeping so funds
  stay segregated (R3 strengthened)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The balance scan is only a trigger: every standard ERC-20 balance
increase has a corresponding Transfer log, fetched per address in
stage 2, which carries the sender to screen - nothing is credited,
screened or swept from a balance observation alone. Consequently the
stage-2 log query is mandatory before scheduling any variant B sweep.
Addresses mixing blocked and clean un-swept deposits are frozen
entirely (R3): no partial sweeps of clean amounts out of an address
holding sanctioned funds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claim_deposit(account, tx_hash): cheapest and most precise detection
(one receipt query per claim, no scanning state), but the worst UX -
the tx hash is only known to the CEX/user and cannot be derived by a
frontend, and internal-transaction ETH withdrawals are not verifiable
from the receipt at all. Rejected as primary; noted as a manual
recovery path complementing the scanning window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gregorydemay and others added 17 commits July 6, 2026 09:24
The blocklist controls what is minted, never what the pool receives:
tainted funds can always be pushed to the main address directly (as
today via the helper), so the sweep exclusion of R3 is best-effort
segregation, not a security boundary. Consequently variant B's
caller-gating is not the only sound option: add a sub-variants table
for principal-spoofing protection - caller-gating, one-time
self-attestation by the deposit key (ecrecover check, ~3k gas,
permissionless sweeps with cryptographically enforced binding), and
on-chain re-derivation (rejected: HMAC-SHA512 in EVM is uneconomical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stuck sweep transaction must never head-of-line-block withdrawals,
which share the main address' single nonce sequence. Sweeps are
therefore issued from a dedicated tECDSA-derived sweeper address
(schema tag 3) holding only gas money; swept funds still land at the
main address (R6). This also simplifies R14: the ckETH burn happens at
funding time (transferred amount + funding fee), and the sweeper
address' balance IS the prepaid_sweep_gas counter, reconcilable
on-chain. Variant B's caller-gating set becomes {SWEEPER, SELF}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A third party - frontend, relayer, or the beneficiary - can call
deposit_erc20(beneficiary, fee_from_subaccount, max_fee): the minter
burns the transaction fee in ckETH from the caller's account (the
existing withdraw_erc20 pattern) and credits the beneficiary the full
deposited amount, no deposit_fee. Satisfies R14 per transaction by
construction, needs no fee-account replenishment on this path, enables
fee sponsorship and on-demand consolidation (R16 synergy). Cannot be
the only path: a fresh CEX-only user owns no ckETH, so it coexists
with the minter-fronted default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two composable pieces: (1) the single funding pipeline (ckETH fee
account -> R14 burn at sweeper funding -> sweeper balance -> gas), the
only way ETH is ever spent on sweeps; (2) what fills the fee account -
deposit_fee revenue, treasury top-ups, and sponsored gas transferred in
by deposit_erc20 callers (icrc2_transfer_from into the fee account,
decoupled in time from the batched burn). R14 stays a single
choke-point invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-document consistency pass. Propagate later decisions backwards:
the sweep sender is the dedicated sweeper address (R17) in the
Motivation, both ckUSDT diagrams and the EIP-7702 primer; the R14 burn
happens at funding time in the diagrams and test plan (sweeper balance
replaces the prepaid_sweep_gas counter); the primer's gating set is
{SWEEPER, SELF}. Also: six steps (not five), R9 gains the sweeper gas
balance, step 4 notes the sponsored-fee waiver, step 5 clarifies the
sweeper derivation path has no account components, an empty fee account
halts refunding (then sweeping), and PR 4 picks up R16 and the
caller-paid endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Funding the sweeper address happens before any sweep exists to pay for
- that is the burn-first property of R14 - so presenting it as the
final step read backwards. The section moves ahead of step 1 as step 0,
framed as a background precondition independent of any individual
deposit; all cross-references renumbered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0) deposit_erc20 sponsored gas transfers the specified ckETH amount to
the minter's fee account. 1) a daily task tracks the fee account
balance on the ledger and the sweeper address' ETH balance, and refills
the sweeper below its low-water mark by withdrawing ckETH from the fee
account to the sweeper address - the existing withdrawal pipeline
(burn, then send), so R14 holds by construction with no new burn path.
Registered decision: the sweeper address is distinct from the minter's
main address (R17).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
owner sv3dd-oaaaa-aaaar-qacoa-cai (ckETH minter), subaccount
0x00..0fee, icrc1_balance_of 1_762_128_000_000_000_000 wei (~1.76
ckETH) as of 2026-07-06: the step 0 funding pipeline starts with
capital already in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registered decision: the endpoint is ERC-20-specific, mirroring the
existing withdraw_eth/withdraw_erc20 split, so whether different tokens
ever get different deposit addresses stays open. Phase 2 adds
retrieve_eth_deposit_address for the schema-2 address.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ring

One endpoint, mirroring withdraw_erc20/withdraw_eth:
deposit_erc20(account, fee?) registers the address, arms the scanning
window and returns the address; the optional fee arguments make the
call sponsored (transfer ckETH to the fee account, on-demand
detection/sweep/crediting). R13 clarified: an unsponsored call spends
nothing; a sponsored call is compensated by the caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deposit_erc20 records the address with the then-current last finalized
block as scan floor (history before registration is never scanned); the
active set is capped (sponsored calls bypass the cap); each address
carries a cycles budget - exhausted means dormant until re-armed, the
scanning window of R15 bounded by cost rather than wall time. Per-token
minimum amounts are a configurable list. Filter 1: Multicall3 balance
reads against the minimums; filter 2: batched eth_getLogs from the
minimum of the candidates' last observed blocks over at most 500
blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mint is triggered by the sweep's finalized helper event through the
existing crediting pipeline. Variant A stays documented so the
trade-offs are not relitigated. R16 holds by construction under B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Digest format with domain separation (prefix collides with no other
signed preimage domain), lifecycle (one tECDSA signature at first
sweep, no nonce consumed, public, audit event), delegate verification
snippet (~3k gas), batching (Multicall3 usable again), the
no-revocation risk and its mitigation (deterministic signing from the
registration map), and why Phase 2 ETH addresses need no attestation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…imer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runnable foundry PoC lives in a separate PR on top of this one so that
this PR contains only the design document. The document's links to the demo
resolve once the follow-up lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gregorydemay gregorydemay marked this pull request as ready for review July 7, 2026 06:41
@gregorydemay gregorydemay requested a review from a team as a code owner July 7, 2026 06:41
@github-actions github-actions Bot added the @defi label Jul 7, 2026
@zeropath-ai

zeropath-ai Bot commented Jul 7, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 3e7dc19.

Security Overview
Detected Code Changes
Change Type Relevant files
Other relevant categories ► rs/ethereum/cketh/docs/deposit_from_cex.md
    Add comprehensive documentation for depositing from CEX via per-account deposit addresses (EIP-7702 sweeping)

…batch)

Filter 2 is one OR-list eth_getLogs, not a JSON-RPC batch; filter 1 is one
Multicall3 aggregate3 eth_call. eth_batch (#561) is a later cycles optimization,
not a correctness dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@mbjorkqvist mbjorkqvist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gregorydemay! I have mostly clarifying questions.

Comment on lines +113 to +121
* `R10`: Withdrawals (ckERC20 → ERC-20 and ckETH → ETH) are unaffected: they continue
to be served from the minter's main address and its existing nonce sequence.
* `R13`: Registering a deposit address (an unsponsored `deposit_erc20` call)
triggers no threshold-ECDSA signature and no Ethereum transaction (a *sponsored*
call may trigger both — compensated by the caller's ckETH fee). The minter only signs a
delegation and sweeps an address after having observed there a balance of a
supported token of at least the per-token minimum deposit amount. (Registrations
are free for callers; anything the minter spends per registration is a DoS vector
on its cycles and ETH.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The numbering jumps from R10 to R13 (R11 and R12 are in "Phase 2" below).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Good catch. The requirements are grouped by phase rather than numbered sequentially (R11/R12 are Phase 2); renumbering would ripple through dozens of R-references across the doc, so instead I added a one-line note under the Phase 1 heading clarifying this. Done in 8c33a9d.

Comment on lines +110 to +112
* `R9`: The minter dashboard and metrics expose: registered deposit addresses,
credited-but-unswept balances per token, delegation status, sweep activity, and the
sweeper address' gas balance (`R17`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "registered deposit addresses" listing them all, or a count? Does the minter store something per (principal, subaccount), in which case there could be a potential DoS attack if someone e.g., asks for lots of deposit addresses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This we will refine as we go, the dashboard is not that critical. For the DoS see the comments below.

Comment on lines +119 to +121
supported token of at least the per-token minimum deposit amount. (Registrations
are free for callers; anything the minter spends per registration is a DoS vector
on its cycles and ETH.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part about registrations (partially) answers my earlier question(s). Can we do something to limit our exposure? E.g., rate limit registrations per principal (in inspect_message?), or something else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I plan to do something similar as what was planed to do for ckSOL: an address as a limited time window (say 24h) and a limited amount of cycles to reach a deposit.

Comment on lines +130 to +135
* `R15`: A single user-visible step suffices: after one `deposit_erc20`
call, a deposit arriving at that address within its *scanning window* is credited
with no further canister call by the user or frontend. Re-calling
`deposit_erc20` (idempotent, free of per-address spending per `R13`)
re-arms the window; a deposit arriving on a dormant address is credited once the
address is re-armed and is never lost in the meantime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could imagine that one use case would involve users depositing to the same address (same (principal, subaccount)) multiple times, but perhaps days apart. For the first deposit, I assume a user would click a "get deposit address" button somewhere (in OISY, or in the MULTI/DEX), which would trigger a call to deposit_erc20 to derive the address (and open the scanning window). However, if the user then simply reuses that address (maybe stored in their address book in a CEX) for a subsequent deposit a week later, they may be confused as to why their deposit is not arriving. Is there something that could be done to improve this flow (other than a reminder/note next to the button the user initially pressed)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could alleviate the problem by making Multicall3 /eth_call to fetch the ERC-20 balances of multiple addresses with a single HTTPs outcall (in the order of 1000s addresses). The limit there will probably the max response size of 2MB and this is per token but this would mean keep making HTTPs outcalls forever as soon as someone called the deposit endpoint. I would say let's me conservative now, and if this is a problem we can always throw more cycles at it.

until confirmed.
* `R6`: A sweep transaction moves funds only to the minter's main address, regardless
of who triggers it. No other destination is reachable through the sweeper delegate.
* `R7`: The per-token `deposit_fee` and minimum deposit amount are configurable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R7 says the fee covers the "amortized sweep gas cost", but two costs aren't captured:

  1. Detection cycles. The background scan (step 3) spends cycles on every armed address whether or not a deposit arrives. At current mainnet gas (~0.1 gwei) a sweep is ~$0.017, while scanning one address for a full window is ~$0.02–0.03 in cycles — i.e. detection currently costs more than the sweep. The fee floor should be solved against detection + sweep, not sweep alone.
  2. Currency mismatch. deposit_fee accrues in the deposited ckToken, but sweep gas is spent in ckETH (from the fee account) and detection in cycles. "Fees cover gas" only holds after the out-of-band treasury conversion noted in Non-goals — worth making explicit here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the costs are currently not clear enough. Basically each address would get a cycle budget to account for

  1. signatures
  2. HTTPs outcalls

Once we have a better understanding of those cost, these will translate into a fee that will be taken by the minter. The currency mismatch is currently outscoped, but should be do able with OISY TRADE.

the active set is full, an unsponsored call still registers and returns the
address but signals that scanning is saturated; a *sponsored* call bypasses the
cap (the caller pays).
* Each active address carries a **cycles budget**, decremented by its share of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window is defined as a cycles budget per tick, but the tick cadence is left unspecified ("bounded by cost rather than wall time" bounds total cost but not detection latency — how fast the budget is spent). Two concrete additions worth pinning down (also relevant to the cadence/DoS questions on the sweep side above):

  • Schedule. A CEX withdrawal usually lands within minutes, so a backoff cadence (e.g. every 30s for 0–10 min, every 2 min to 30 min, every 5 min to 60 min ≈ 36 ticks) covers the likely-arrival window with ~40% fewer calls than a flat 1/min (60 ticks).
  • Cost. Detection goes through the EVM-RPC canister on the 34-node fiduciary subnet × 3 providers: base ≈ 0.8B cycles/call ($0.0011) plus byte terms (~$0.0003/req-byte, ~$0.0001/resp-byte; 1T cycles = 1 XDR ≈ $1.37). A balanceOf tick over the active set is ~$0.0003–0.0005 per address per tick, so a full 60-min window is ~$0.017–0.03 per address (T≈3–5 tokens).

Key economic point: detection cycles are spent on every armed address whether or not a deposit ever arrives, but the fee is only collected on successful deposits — so empty/abandoned windows are pure minter loss. cap × per-address-budget is the real bound (e.g. 256 addrs × ~15B cycles ≈ 3.8T cycles ≈ $5.25 to re-arm the whole set once). Stating concrete cap / budget / cadence values would let the fee floor and the DoS ceiling both be solved explicitly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ethereum blocks are produced every 12s, so something like 30s, 1min, 2 min, 4min,... seems to make sense the exact schedule is dependent of the cost calculation

| Variant | Pros | Cons |
|---|---|---|
| **A — mint on the finalized deposit** (new detection→mint path) | Lowest, sweep-independent crediting latency; crediting keeps working even when sweeping halts (e.g. empty `R14` fee account); permissionless-safe sweeps (step 5) | A second correctness-critical crediting path in the minter: new event types, dedup, audit trail — the highest-risk part of the feature. Opens a **liquidity window**: supply is minted while the backing still sits at deposit addresses, so a withdrawal in that window could exceed the main address' balance — not a solvency issue (backing is minter-controlled throughout) but withdrawals must treat credited-but-unswept amounts as unavailable and queue accordingly (`R16`), and sweeps should be prioritized by withdrawal demand |
| **B — mint via the existing pipeline, on the sweep's own finalized helper event** | The battle-tested scrape→parse→dedup→mint pipeline is reused **unchanged** — detection (step 3) is demoted from correctness-critical to a mere scheduling hint; much smaller minter change. No liquidity window: the mint is triggered by the consolidation itself, so minted supply is always covered by the main address — today's helper-flow invariant, and `R16` is satisfied trivially | Mint follows the sweep: crediting halts if sweeping halts (empty fee account); latency tied to sweep scheduling — mitigated by sweeping on `latest`-block observations without waiting for deposit finality (a reorged deposit only wastes the sweep's gas: the delegate sweeps a zero balance, and a reorged sweep tx is absorbed by the existing nonce-tracking/resubmission machinery), making end-to-end latency comparable to today's helper flow |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth surfacing at requirement level, not just in this cell's cons: because variant B mints from the sweep's helper event, crediting is coupled to sweeping. A gas spike or an empty ckETH fee account halts not just sweeps but all new ckERC20 CEX crediting (funds stay safe at key-controlled addresses, but new deposits stop being credited until the fee account is topped up / gas subsides). R5's "a sweep delay never affects already-minted balances" holds for already-minted balances, but new deposits stall — this is the main operational cost of choosing B.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 A Cost estimation section quantifying variant B’s operational cost was added in 3e7dc19 (tECDSA + outcalls + gas across four scenarios). The crediting-coupled-to-sweeping point you raise is called out today in step 0 (“under variant B, crediting pauses with sweeping”) and in the variant-B cons cell; elevating it to an explicit requirement/non-goal is still open and a good follow-up.

deployed delegate instance doubling as the batcher. No deposit address ever needs
an ETH balance for gas. A periodic task selects addresses with
observed-but-unswept balances where `unswept_value ≥ sweep_gas_cost × margin` or
`age > max_age` — or, under variant A, **as soon as a queued withdrawal waits for

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

age > max_age is effectively R5's liveness net: it fires when unswept_value never crosses the economic threshold on its own — chiefly during gas spikes (which inflate sweep_gas_cost × margin above the deposit value) or for a lone below-margin balance waiting to be batched. Two notes:

  • Under decided variant B this also bounds crediting latency, not just consolidation: since the mint follows the sweep, a below-margin deposit isn't credited until age > max_age forces the sweep. So max_age is the worst-case "my ckToken hasn't arrived yet" time for small deposits during a spike.
  • max_age and margin are unspecified (as are the step-3 window cap / budget / cadence).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Agreed, worth making explicit. Added to step 5: under decided variant B the mint follows the sweep, so max_age doubles as the worst-case crediting latency for a below-margin deposit (credited only once age forces its sweep, chiefly during gas spikes that keep unswept_value under sweep_gas_cost × margin). Done in 8c33a9d.

| **A — direct sweep** (`CkSweeper`): delegate transfers straight to the minter's main address | Permissionless-safe: destination hardcoded (`R6`), any caller only donates gas → no access control in the delegate (with the `R3` caveat that a third party may sweep a tainted address — no worse than today); cheapest — measured 66'854 gas for a first single-address sweep incl. authorization, ≈ 26k marginal per additional address in a batch | Requires the new crediting path of step 4 variant A |
| **B — sweep through the helper** (`CkSweeperViaHelper`): delegate approves + calls `depositErc20(token, balance, principal, subaccount)` on the existing helper | Sweep emits the canonical `ReceivedEthOrErc20` event → step 4 variant B's pipeline reuse; native ETH works symmetrically via `depositEth` | The principal is a sweep argument → it must be protected against spoofing (an arbitrary caller crediting a deposit to their own principal) by caller-gating or a one-time attestation — see the sub-variants below; more gas — measured 82'207 single (+15'353), 164'746 for a batch re-delegating three EOAs and sweeping two |

**Sub-variants (variant B only) — how to prevent principal spoofing:**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anti-spoofing sub-variant reads as undecided while variant B itself is decided: the delegate in Sweeper delegate contract and the demo both implement caller-gating, but this table declares no winner (unlike the other variant tables). The choice is cross-cutting — it determines whether sweeps are permissionless (self-attestation) or minter-only (caller-gating), and whether R3's best-effort sweep-exclusion survives. Recommend either marking a decision here or explicitly flagging it as the one open sub-decision.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to permissionless with self-attestation but want to have a better understanding of the cost involved first (tECDSA, gas, sweeper contract complexity)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The cost understanding this decision was waiting on is now in the doc: the Cost estimation section (3e7dc19) sizes tECDSA signatures, HTTPS outcalls and gas, and notes the self-attestation sub-variant adds one tECDSA signature (≈ $0.036, one-time per address) to each address’ first sweep. So the sub-decision can now be made on cost grounds. It is tracked as the one open sub-decision (Jira DEFI-2918).

1. **Get the balance**: `balanceOf(address(this))`. The whole current balance is
swept; there is no partial sweep.
2. **Approve the balance**: approve the helper for exactly that balance, through a
USDT-tolerant `_safeApprove` (USDT's `approve` returns no value and reverts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sweep assumes balanceOf(address(this)) equals the amount that will actually transfer, and that depositErc20 moves exactly balance. That holds for standard tokens (USDC, USDT today) but not for fee-on-transfer / rebasing ERC-20s. Probably out of scope (same assumption as today's helper flow), but a one-line "supported tokens are assumed standard, non-fee-on-transfer" would close it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Correct, and it is the same standard-token assumption as today’s helper flow. Added a note to the sweeper delegate section: supported tokens are assumed non-fee-on-transfer and non-rebasing, so balanceOf(address(this)) equals the amount depositErc20 transfers. Done in 8c33a9d.

gregorydemay and others added 2 commits July 7, 2026 12:49
…cy, token assumptions, R-numbering)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gregorydemay added a commit that referenced this pull request Jul 7, 2026
Stacked on top of the design-doc PR (#10652). Adds the runnable alloy/foundry
demo of the EIP-7702 sweep mechanism (both variants, gas assertions, attacker
rejection) that the design document references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants