docs: design for supporting deposits from CEX (DEFI-2096)#10652
Open
gregorydemay wants to merge 39 commits into
Open
docs: design for supporting deposits from CEX (DEFI-2096)#10652gregorydemay wants to merge 39 commits into
gregorydemay wants to merge 39 commits into
Conversation
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>
Contributor
There was a problem hiding this comment.
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.
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>
gregorydemay
commented
Jul 3, 2026
gregorydemay
commented
Jul 3, 2026
gregorydemay
commented
Jul 3, 2026
gregorydemay
commented
Jul 3, 2026
gregorydemay
commented
Jul 3, 2026
gregorydemay
commented
Jul 3, 2026
- 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>
gregorydemay
commented
Jul 6, 2026
gregorydemay
commented
Jul 6, 2026
gregorydemay
commented
Jul 6, 2026
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>
CEX withdrawal, off-chain authorization signing, first sweep with the three-phase type-0x04 processing, designator persistence (plain type-2 re-sweeps), and the batched sweep with the delegate's dual execution context - including why configuration must live in immutables rather than storage, the skipped-tuple/atomic-revert failure mode, and why single-transaction delegation is not possible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One filter covers all active deposit addresses across all supported tokens: address = OR-list of token contracts, topics[2] = OR-list of padded recipient addresses. This makes the log query the primary ERC-20 detector (reusing the existing Topic::Multiple machinery), with balance scans kept for native ETH (no logs) and O(1)-per-address re-checks of dormant sets. Clarify that the continuous-scraping variant was rejected for its unbounded address set, not the mechanism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Minting on the finalized deposit means supply can temporarily exceed the main address' balance while the backing sits at deposit addresses - not a solvency issue (backing stays minter-controlled) but a withdrawal in that window could fail on-chain for insufficient balance. New R16: credited-but-unswept amounts are unavailable liquidity, uncovered withdrawals are queued, and withdrawal demand overrides the sweep scheduling thresholds. Variant B satisfies R16 trivially (the mint is triggered by the consolidation itself) - added to its pros. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
|
✅ No security or compliance issues detected. Reviewed everything up to 5ab23ba. Security Overview
Detected Code Changes
|
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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