feat(cketh): install a deposit address' delegation with its first sweep - #11250
Draft
gregorydemay wants to merge 5 commits into
Draft
feat(cketh): install a deposit address' delegation with its first sweep#11250gregorydemay wants to merge 5 commits into
gregorydemay wants to merge 5 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds EIP-7702 delegation installation to first-time ckETH deposit-address sweeps while generalizing transaction pipeline mechanics.
Changes:
- Introduces EIP-1559/EIP-7702 sweep transactions.
- Generalizes finalization and fee resubmission.
- Extends audit events and Candid types with signed authorizations.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/dump_stable_memory.rs |
Maps new sweep events. |
src/tx/tests.rs |
Tests sweep encoding and signing. |
src/tx/sweep.rs |
Defines variant sweep transactions. |
src/tx/signed.rs |
Expands the signable transaction interface. |
src/tx/mod.rs |
Generalizes fee resubmission. |
src/tx/finalized.rs |
Adds generic finalized transactions. |
src/tx/eip_7702.rs |
Enables EIP-7702 fee bumps. |
src/tx/eip_1559.rs |
Adopts generic transaction machinery. |
src/state/transactions/tests.rs |
Tests delegating sweep pipelines. |
src/state/transactions/request.rs |
Creates the appropriate sweep variant. |
src/state/transactions/mod.rs |
Generalizes transaction pipelines. |
src/state/event.rs |
Stores sweep transaction variants. |
src/state/audit/tests.rs |
Updates event replay mapping. |
src/main.rs |
Exposes authorization data in events. |
src/endpoints.rs |
Adds Candid-facing sweep types. |
cketh_minter.did |
Updates the public Candid interface. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+191
to
+194
| fn map_signed_sweep_transaction(raw_transaction: &str) -> SignedSweepTransaction { | ||
| let (transaction, signature) = decode_signed_transaction(raw_transaction); | ||
| SignedSweepTransaction::from((SweepTransaction::Eip1559(transaction), signature)) | ||
| } |
Comment on lines
+248
to
+251
| fn map_signed_sweep_transaction(raw_transaction: &str) -> SignedSweepTransaction { | ||
| let (transaction, signature) = decode_signed_transaction(raw_transaction); | ||
| SignedSweepTransaction::from((SweepTransaction::Eip1559(transaction), signature)) | ||
| } |
gregorydemay
force-pushed
the
greg/sweeper-eip7702
branch
from
August 21, 2026 08:21
02b7635 to
67bd529
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rs/ethereum/cketh/minter/tests/dump_stable_memory.rs:193
- Delegating sweeps are signed type-0x04 transactions, but this path always calls an EIP-1559-only decoder and then constructs
SweepTransaction::Eip1559. Once a signed delegating sweep appears inget_events,decode_signed_transactionrejects it at itsTypedTransaction::Eip1559match, so stable-memory dumping cannot process the new event. Decode type 0x04 (including its authorization list) and construct the matching sweep variant.
fn map_signed_sweep_transaction(raw_transaction: &str) -> SignedSweepTransaction {
let (transaction, signature) = decode_signed_transaction(raw_transaction);
SignedSweepTransaction::from((SweepTransaction::Eip1559(transaction), signature))
rs/ethereum/cketh/minter/src/state/audit/tests.rs:250
- This replay mapper still accepts only
TypedTransaction::Eip1559and unconditionally rebuilds the signed sweep as that variant. A signed first sweep is type 0x04, so refreshing/replaying events after delegating sweeps begin will panic instead of reconstructing state. Decode EIP-7702 raw transactions and preserve their authorization list here.
fn map_signed_sweep_transaction(raw_transaction: &str) -> SignedSweepTransaction {
let (transaction, signature) = decode_signed_transaction(raw_transaction);
SignedSweepTransaction::from((SweepTransaction::Eip1559(transaction), signature))
gregorydemay
force-pushed
the
greg/sweeper-eip7702
branch
3 times, most recently
from
August 21, 2026 08:54
86f084b to
62aa850
Compare
gregorydemay
force-pushed
the
greg/sweeper-eip7702
branch
from
August 21, 2026 09:13
62aa850 to
afd8ef3
Compare
Finalization and fee-bumping were written against EIP-1559 alone: `FinalizedEip1559Transaction` was a concrete struct, `try_finalize` an inherent method on `SignedEip1559TransactionRequest`, and `resubmit` an inherent method on `Resubmittable<SignedEip1559TransactionRequest>`. The sweeper pipeline will carry type-`0x04` transactions to install a deposit address' EIP-7702 delegation, and would have needed all three mirrored for it — the very TODO `SignedEip7702TransactionRequest` carried. All three reduce to one primitive. Give `SignableTransaction` its remaining field accessors and `with_price_and_amount`, and everything else follows generically: `Finalized<T>` replaces the concrete struct with `FinalizedEip1559Transaction` as its alias, `try_finalize` moves to `Signed<T>`, `resubmit` to `Resubmittable<Signed<T>>`, and `equal_ignoring_fee_and_amount` takes any two transactions of one type. EIP-7702 gets fee-bumping for free: an authorization is signed over `(chain_id, delegate, nonce)` only, so bumping the outer fee leaves the authorization list valid, and struct update syntax carries it along. `Finalized<T>` bounds `T: SignableTransaction` on the struct, as `TransactionPipeline<R: PipelineRequest>` does: a finalized transaction can only come from a signed one, and the derived `Decode` needs the bound to see through `Signed<T>`. No behaviour change. `equal_ignoring_fee_and_amount` still compares `gas_limit`, taking it from the right-hand transaction while overriding only the two fees and the amount. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pipeline was generic over its request but not over what that request becomes: `created_tx`, `sent_tx` and `finalized_tx` were pinned to `Eip1559TransactionRequest`, as were `create_transaction`, `assert_created_transaction`, `TransactionStage` and every accessor between them. The sweeper pipeline needs to carry type-`0x04` transactions to install a deposit address' EIP-7702 delegation, and cannot while the pipeline names the transaction type itself. Add `PipelineRequest::Transaction` and let the pipeline speak it throughout: a request already knows the transaction it turns into, so it is the request that should say so. The associated type requires `SignableTransaction` and nothing more, since that is all a request needs to build one. `Clone + Eq + Debug` are required by the pipeline's impl block instead, which is what clones transactions into signing batches and compares them in its assertions. Both pipelines still set `Transaction = Eip1559TransactionRequest`, so this changes no behaviour: every stored type, event and call site resolves to exactly what it did before. The sweeper's own transaction type comes next. `CreatedTransaction<R>` and `SentTransaction<R>` name the two `Resubmittable` nestings the pipeline stores, taking over from the EIP-1559-pinned `TransactionRequest`/`SignedTransactionRequest` aliases, which stay in `tx` for the withdrawal-lane tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A deposit address holds ERC-20 tokens but no code, so it cannot sweep itself. The minter delegates it to the sweeper contract with an EIP-7702 authorization, and the cheapest place to carry that authorization is the sweep that needs it: the first sweep touching an address is a type-`0x04` transaction that installs the delegation on the way. The delegation then persists, so every later sweep of that address is a plain type-`0x02` transaction — which is what `deposit_from_cex_demo` measures, at 94'932 gas for the first sweep of one address against 63'252 for the next. `SweepTransaction` is what the sweeper pipeline now carries: an EIP-1559 transaction, or an EIP-7702 one whose authorization list installs the delegations. `SweepRequest::authorizations` holds them, signed, so `create_transaction` stays a pure function of the request and the accepted event carries them — a replay never re-signs, and neither does a fee bump, since an authorization covers `(chain_id, delegate, nonce)` only and nothing of the outer transaction. `SweepTransaction::new` is the single place that picks the variant, and it picks it from whether there is anything to install. That keeps the two representations in step: the `Eip7702` variant always carries a non-empty authorization list, which is what its RLP encoding already asserts, and the Candid mirror can carry the list next to the transaction without a tag of its own. The three sweeper transaction events change shape accordingly, with `UnsignedSweeperTransaction` as their Candid mirror rather than a widened `UnsignedTransaction`: the withdrawal events on mainnet keep the type they have today, untouched. `AcceptedSweepRequest` mirrors the delegations too. Which addresses still need delegating is not decided here — nothing enqueues a sweep yet. Because the request carries signed authorizations rather than a flag, that decision belongs entirely to the sweep-queue source, together with reading each address' delegation from the chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gregorydemay
force-pushed
the
greg/sweeper-eip7702
branch
from
August 21, 2026 09:55
afd8ef3 to
e268990
Compare
The `SignedSweeperTransaction` audit event records only the raw hex of the broadcast transaction, and the reverse mapping used to replay event dumps decoded it with `ethers_core`, which predates EIP-7702 and only understands type `0x02`. A genuine type-`0x04` sweep would therefore be replayed as an EIP-1559 transaction and compare unequal to the recorded one. Add the inverse of the type-`0x04` RLP encoder next to the encoder itself, and let both reverse mappings dispatch on the transaction type byte so a sweep is rebuilt as the variant it was actually sent as. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <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.
Part of DEFI-2917 (deposit-from-CEX), stacked on #11144, which gives the sweeper address the pipeline this teaches a second transaction type.
Why
A deposit address holds ERC-20 tokens but no code, so it cannot sweep itself. The minter delegates it to the sweeper contract with an EIP-7702 authorization, and the cheapest place to carry that authorization is the sweep that needs it: the first sweep touching an address installs the delegation on the way, and the delegation then persists, so every later sweep of that address is a plain EIP-1559 transaction.
deposit_from_cex_demomeasures 94'932 gas for the first sweep of one address against 63'252 for the next.What
Two refactorings first, each a no-op: finalization and fee-bumping stop being EIP-1559-only, and a request names the transaction it turns into rather than the pipeline naming one type for both lanes. Both reduce to a single new primitive — a transaction that can restate itself at a different price — which is also what gives EIP-7702 fee-bumping for free, with no machinery of its own.
Then the sweeper lane carries either transaction type, and a sweep carries the signed authorizations it must install. Holding them in the request keeps building the transaction a pure function of it and puts them in the accepted event, so neither a replay nor a fee bump ever re-signs one: an authorization covers the chain, the delegate and the authority's nonce, and nothing of the outer transaction. One place decides which type a sweep becomes, from whether there is anything left to install, which is what keeps the two representations in step.
Scope
Nothing enqueues a sweep yet, so nothing builds an authorization in production. Which addresses still need delegating is deliberately left to the sweep-queue source, along with reading each address' delegation from the chain — the request carries signed authorizations rather than a "needs delegation" flag precisely so that decision lives there. The batch-dependent gas limit a delegating sweep needs is deferred with it; #11237's flat 100'000 does not cover one.
Tests
A two-variant transaction type goes wrong by signing the wrong preimage, so both variants are checked to hash and to encode byte-identically to the transaction they wrap, with golden CBOR for each since the event log stores them. On the pipeline side: which variant a request produces, delegations surviving a fee bump, and finalizing a sweep that installed them.
Candid compatibility
Two new type definitions, used only by the sweeper events #11144 adds and that therefore exist nowhere on mainnet. The
UnsignedTransactioncarried by the mainnet withdrawal events is left alone rather than widened with an optional authorization list, and the call data and signature components areblobs like their neighbours.Stack created with GitHub Stacks CLI • Give Feedback 💬