@@ -53,7 +53,8 @@ const MAX_CODE_SIZE: usize = 24_576;
5353/// [EIP-3860]: https://eips.ethereum.org/EIPS/eip-3860
5454const MAX_INITCODE_SIZE : usize = 2 * MAX_CODE_SIZE ;
5555
56- /// Maximum number of balance reads in a single deployless-batcher `eth_call`, for both batchers.
56+ /// Maximum number of entries in a single deployless-batcher `eth_call`, shared by all three
57+ /// batcher programs.
5758///
5859/// A create-style `eth_call` is bounded at both ends: the initcode it carries is rejected beyond
5960/// [`MAX_INITCODE_SIZE`] (EIP-3860), and the blob the program `RETURN`s is rejected beyond
@@ -74,11 +75,13 @@ const MAX_INITCODE_SIZE: usize = 2 * MAX_CODE_SIZE;
7475/// whole-call failure re-does that chunk on the next tick — and a chunk that always exceeds a
7576/// provider limit fails *every* time, permanently stalling its pairs.
7677///
77- /// The value is derived from the ERC-20 encoding, yet it caps [`ETH_BATCHER_INITCODE`] batches
78- /// too, with margin on both ends. The returned-blob term is shared — both programs return one
79- /// word per entry — so the cap can never exceed `MAX_CODE_SIZE / WORD`; and the ETH initcode
80- /// side is far looser than the ERC-20 one (one word per entry after a 78-byte program, so 1532
81- /// reads) and never binds. `full_batch_of_eth_balance_reads_fits_both_node_limits` pins this.
78+ /// The value is derived from the ERC-20 encoding, yet it caps [`ETH_BATCHER_INITCODE`] and
79+ /// [`DELEGATION_BATCHER_INITCODE`] batches too, with margin on both ends. The returned-blob term
80+ /// is shared — all three programs return exactly one word per entry — so the cap can never
81+ /// exceed `MAX_CODE_SIZE / WORD`; and both single-word-argument programs have a far looser
82+ /// initcode side than the ERC-20 one (one word per entry after a 78- resp. 81-byte program, so
83+ /// 1532 entries) that never binds. `full_batch_of_eth_balance_reads_fits_both_node_limits` and
84+ /// `full_batch_of_delegation_reads_fits_both_node_limits` pin this.
8285pub const MAX_CALLS_PER_BATCH : usize = {
8386 let by_initcode_size = ( MAX_INITCODE_SIZE - BATCHER_INITCODE . len ( ) - WORD ) / ( 2 * WORD ) ;
8487 let by_returned_code_size = MAX_CODE_SIZE / WORD ;
@@ -109,6 +112,38 @@ pub const ETH_BATCHER_INITCODE: [u8; 78] = [
109112 0x61 , 0x00 , 0x0d , 0x56 , 0x5b , 0x60 , 0x00 , 0x51 , 0x60 , 0x20 , 0x02 , 0x60 , 0x60 , 0xf3 ,
110113] ;
111114
115+ /// Deployless delegation-batcher creation bytecode.
116+ ///
117+ /// The sibling of [`ETH_BATCHER_INITCODE`] reading code instead of balances, executed the same
118+ /// way (create-style `eth_call`, `to` omitted). It reads its inputs from the calldata appended
119+ /// right after this bytecode (`[n][ address x n ]`, one 32-byte word each) and, for each address,
120+ /// copies the first 32 bytes of its code with `EXTCODECOPY` — which zero-pads beyond the code
121+ /// size, so every entry is a full word whether the account holds no code, an EIP-7702 designator
122+ /// (23 bytes) or a contract. No sub-calls, so like the ETH batcher nothing here can fail and the
123+ /// program has no revert path. It returns the prefixes as a flat `n x 32`-byte array (no ABI
124+ /// array header), classified positionally by [`decode_delegation_batch`].
125+ ///
126+ /// The program is fixed regardless of `n` (only the appended args grow). It was assembled from
127+ /// the opcode listing in `delegation_initcode_matches_readable_assembly` and validated against a
128+ /// live anvil node; see `rs/ethereum/cketh/minter/tests/deposit_from_cex.rs`.
129+ pub const DELEGATION_BATCHER_INITCODE : [ u8 ; 81 ] = [
130+ 0x60 , 0x20 , 0x61 , 0x00 , 0x51 , 0x60 , 0x00 , 0x39 , 0x60 , 0x00 , 0x60 , 0x20 , 0x52 , 0x5b , 0x60 , 0x00 ,
131+ 0x51 , 0x60 , 0x20 , 0x51 , 0x10 , 0x15 , 0x61 , 0x00 , 0x47 , 0x57 , 0x60 , 0x20 , 0x60 , 0x20 , 0x51 , 0x60 ,
132+ 0x20 , 0x02 , 0x61 , 0x00 , 0x71 , 0x01 , 0x60 , 0x40 , 0x39 , 0x60 , 0x20 , 0x60 , 0x00 , 0x60 , 0x20 , 0x51 ,
133+ 0x60 , 0x20 , 0x02 , 0x60 , 0x60 , 0x01 , 0x60 , 0x40 , 0x51 , 0x3c , 0x60 , 0x20 , 0x51 , 0x60 , 0x01 , 0x01 ,
134+ 0x60 , 0x20 , 0x52 , 0x61 , 0x00 , 0x0d , 0x56 , 0x5b , 0x60 , 0x00 , 0x51 , 0x60 , 0x20 , 0x02 , 0x60 , 0x60 ,
135+ 0xf3 ,
136+ ] ;
137+
138+ /// Prefix of the code an EIP-7702 delegation designator installs at an authority, per
139+ /// [EIP-7702]: the code of a delegated account is exactly `0xef0100 || delegate`.
140+ ///
141+ /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
142+ const DELEGATION_DESIGNATOR_PREFIX : [ u8 ; 3 ] = [ 0xef , 0x01 , 0x00 ] ;
143+
144+ /// Length of an EIP-7702 delegation designator: its 3-byte prefix plus the 20-byte delegate.
145+ const DELEGATION_DESIGNATOR_LEN : usize = DELEGATION_DESIGNATOR_PREFIX . len ( ) + 20 ;
146+
112147/// Function selector for `balanceOf(address)`, i.e. `keccak256("balanceOf(address)")[..4]`.
113148/// Embedded in [`BATCHER_INITCODE`] right after its leading `PUSH32` opcode; asserted by tests.
114149#[ cfg( test) ]
@@ -179,6 +214,80 @@ pub fn decode_balance_batch(ret: &[u8], n: usize) -> Result<Vec<Erc20Value>, Bat
179214 Ok ( balances)
180215}
181216
217+ /// What the code at a deposit address says about its EIP-7702 delegation.
218+ #[ derive( Clone , Copy , Eq , PartialEq , Debug ) ]
219+ pub enum Delegation {
220+ /// The address holds no code, so no delegation is installed.
221+ NotDelegated ,
222+ /// The address holds an EIP-7702 delegation designator naming this delegate.
223+ Delegated ( Address ) ,
224+ /// The address holds code that is not a delegation designator, i.e. a deployed contract.
225+ /// Such an address must never be swept: a tuple cannot be applied to it.
226+ Other ,
227+ }
228+
229+ /// Build the create-call `input` for a batch of delegation reads:
230+ /// `DELEGATION_BATCHER_INITCODE ++ [n] ++ [ address x n ]`, every value a 32-byte word.
231+ pub fn encode_delegation_batch ( addresses : & [ DepositAddress ] ) -> Vec < u8 > {
232+ let mut out =
233+ Vec :: with_capacity ( DELEGATION_BATCHER_INITCODE . len ( ) + WORD * ( 1 + addresses. len ( ) ) ) ;
234+ out. extend_from_slice ( & DELEGATION_BATCHER_INITCODE ) ;
235+ out. extend_from_slice ( & word_from_usize ( addresses. len ( ) ) ) ;
236+ for address in addresses {
237+ out. extend_from_slice ( & left_padded_address ( address. as_address ( ) ) ) ;
238+ }
239+ out
240+ }
241+
242+ /// Decode the flat `n x 32`-byte return blob of [`DELEGATION_BATCHER_INITCODE`] into `n`
243+ /// delegations, in call order.
244+ ///
245+ /// Each word is the first 32 bytes of an account's code, zero-padded beyond its size, which
246+ /// classifies the account unambiguously:
247+ /// * an all-zero word means no code at all, since [EIP-3541] forbids deploying code starting
248+ /// with `0xef` and a shorter non-empty code would still carry a non-zero first byte;
249+ /// * `0xef0100 || delegate || 9 zero bytes` is a code of exactly 23 bytes whose only possible
250+ /// origin is an applied [EIP-7702] authorization tuple — again because EIP-3541 keeps every
251+ /// deployed contract out of the `0xef` space;
252+ /// * anything else is deployed contract code.
253+ ///
254+ /// Returns `Err` if the blob length is not exactly `n` words; never panics.
255+ ///
256+ /// [EIP-3541]: https://eips.ethereum.org/EIPS/eip-3541
257+ /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
258+ pub fn decode_delegation_batch (
259+ ret : & [ u8 ] ,
260+ n : usize ,
261+ ) -> Result < Vec < Delegation > , BatcherDecodeError > {
262+ let expected = n * WORD ;
263+ if ret. len ( ) != expected {
264+ return Err ( BatcherDecodeError :: WrongLength {
265+ expected,
266+ got : ret. len ( ) ,
267+ } ) ;
268+ }
269+ Ok ( ret
270+ . chunks_exact ( WORD )
271+ . map ( |word| classify_code_prefix ( word. try_into ( ) . expect ( "BUG: chunk is exactly one word" ) ) )
272+ . collect ( ) )
273+ }
274+
275+ fn classify_code_prefix ( prefix : & [ u8 ; WORD ] ) -> Delegation {
276+ if prefix. iter ( ) . all ( |byte| * byte == 0 ) {
277+ return Delegation :: NotDelegated ;
278+ }
279+ let ( designator, padding) = prefix. split_at ( DELEGATION_DESIGNATOR_LEN ) ;
280+ let is_designator = designator. starts_with ( & DELEGATION_DESIGNATOR_PREFIX )
281+ && padding. iter ( ) . all ( |byte| * byte == 0 ) ;
282+ if !is_designator {
283+ return Delegation :: Other ;
284+ }
285+ let delegate: [ u8 ; 20 ] = designator[ DELEGATION_DESIGNATOR_PREFIX . len ( ) ..]
286+ . try_into ( )
287+ . expect ( "BUG: a designator holds exactly 20 address bytes" ) ;
288+ Delegation :: Delegated ( Address :: new ( delegate) )
289+ }
290+
182291fn left_padded_address ( address : & Address ) -> [ u8 ; WORD ] {
183292 let mut word = [ 0_u8 ; WORD ] ;
184293 word[ WORD - 20 ..] . copy_from_slice ( address. as_ref ( ) ) ;
0 commit comments