docs: design for supporting deposits from CEX (DEFI-2096)#10652
docs: design for supporting deposits from CEX (DEFI-2096)#10652gregorydemay wants to merge 13 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>
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.
| directly to the minter address today are simply unaccounted, with no recovery path | ||
| (see the comment on `EthBalance::eth_balance` in `src/state.rs`). |
There was a problem hiding this comment.
🤖 Fixed in cc57879: the doc now references the documentation of the eth_balance field of the EthBalance struct in src/state.rs.
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>
| const MINTER_PK: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; | ||
| const CEX_PK: &str = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; |
There was a problem hiding this comment.
nit: PK can be misunderstood for PUBLICK_KEY, which is not the case here. remove the abbrevation -> MINTER_PRIVATE_KEY
There was a problem hiding this comment.
🤖 Renamed to MINTER_PRIVATE_KEY / CEX_PRIVATE_KEY in cc57879.
| let mut wallet = EthereumWallet::from(minter_signer); | ||
| wallet.register_signer(cex_signer); |
There was a problem hiding this comment.
what has the minter_signer anything to do with the cex_signer? Shouldn't they be 2 separate wallets?
There was a problem hiding this comment.
🤖 Agreed — they are unrelated parties. Split into two separate wallets/providers (minter_wallet/minter_provider and cex_wallet/cex_provider) in cc57879.
| nonce: 0, | ||
| }; | ||
| let signature = deposit_signer.sign_hash_sync(&authorization.signature_hash())?; | ||
| Ok(authorization.into_signed(signature)) |
There was a problem hiding this comment.
nit: print the full hex of the send transaction
There was a problem hiding this comment.
🤖 Done in cc57879: each sweep now prints the full raw signed transaction hex (type-0x04 payload including the authorization list) before sending it.
| ensure!( | ||
| (60_000..=90_000).contains(&single_gas), | ||
| "unexpected single-sweep gas: {single_gas}" | ||
| ); |
There was a problem hiding this comment.
It should be deterministic, let's hard-code the gas used.
There was a problem hiding this comment.
🤖 Hard-coded in cc57879 as SINGLE_SWEEP_GAS_USED = 66_889, asserted with equality.
| let batch_gas = batch_receipt.gas_used; | ||
| ensure!( | ||
| batch_gas < 3 * single_gas, | ||
| "batching brought no amortization: {batch_gas} vs 3 x {single_gas}" | ||
| ); |
There was a problem hiding this comment.
also here hard-code the gas used.
There was a problem hiding this comment.
🤖 Hard-coded in cc57879 as BATCH_SWEEP_GAS_USED = 118_982, asserted with equality.
| ok("deposit addresses still have 0 ETH (cannot pay gas themselves)"); | ||
|
|
||
| step("4) Minter sweeps ONE deposit address in ONE EIP-7702 transaction"); | ||
| let single_authorization = sign_delegation(&deposit_signers[0], sweeper_address, chain_id)?; |
There was a problem hiding this comment.
Let's print the minter's nonce every time the minter signs something (number of transactions sent by the minter is critical)
There was a problem hiding this comment.
🤖 Done in cc57879: the minter's nonce is printed for every transaction it signs (CkSweeper deployment and both sweeps; the CEX deployment nonce is printed too for completeness).
- 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>
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).
🤖 Generated with Claude Code