Skip to content

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

Open
gregorydemay wants to merge 39 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 39 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 20 commits July 6, 2026 08:48
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>
@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 5ab23ba.

Security Overview
Detected Code Changes
Change Type Relevant files
Other relevant categories ► 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>
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.

2 participants