Skip to content

Commit afea681

Browse files
gregorydemayclaude
andcommitted
test(cketh): run the delegation batcher against a live anvil node
Covers the three classifications against a real EVM (bare address, an address carrying a delegation designator, a deployed ERC-20) in both argument orders, and pins that a full batch of MAX_CALLS_PER_BATCH addresses executes in one create-style eth_call while the node's own code-size ceiling sits well above it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X44H4nAU65nAexEUvG15MK
1 parent bbf71fb commit afea681

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

rs/ethereum/cketh/minter/tests/deposit_from_cex.rs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
1010
use assert_matches::assert_matches;
1111
use ic_cketh_minter::balance_scan::batcher::{
12-
BalanceOfCall, MAX_CALLS_PER_BATCH, decode_balance_batch, encode_balance_batch,
13-
encode_eth_balance_batch,
12+
BalanceOfCall, Delegation, MAX_CALLS_PER_BATCH, decode_balance_batch, decode_delegation_batch,
13+
encode_balance_batch, encode_delegation_batch, encode_eth_balance_batch,
1414
};
1515
use ic_cketh_minter::deposit_address::DepositAddress;
1616
use ic_cketh_minter::endpoints::events::EventPayload;
1717
use ic_cketh_minter::endpoints::{DepositEthStatus, DepositStatus};
1818
use ic_cketh_minter::numeric::Erc20Value;
1919
use ic_cketh_test_utils::anvil::{
20-
Anvil, DEV_ACCOUNT, SentTransaction, address_from_hex, deploy_mock_erc20,
20+
Anvil, DEV_ACCOUNT, SentTransaction, address_from_hex, delegation_designator, deploy_mock_erc20,
2121
};
2222
use ic_cketh_test_utils::ckerc20::{CkErc20Setup, Erc20Token};
2323
use ic_cketh_test_utils::live::{
@@ -243,6 +243,88 @@ fn should_read_many_eth_balances_in_a_single_call() {
243243
assert_eq!(balances, expected);
244244
}
245245

246+
#[test]
247+
fn should_read_delegations_across_addresses() {
248+
let anvil = Anvil::start();
249+
let dev = address_from_hex(DEV_ACCOUNT);
250+
251+
let bare = DepositAddress::new(Address::new([0x11; 20]));
252+
let delegated = DepositAddress::new(Address::new([0x22; 20]));
253+
let delegate = Address::new([0xab; 20]);
254+
anvil.set_code(delegated.as_address(), &delegation_designator(&delegate));
255+
let contract = DepositAddress::new(deploy_mock_erc20(&anvil, &dev));
256+
257+
let read = |addresses: &[DepositAddress]| -> Vec<Delegation> {
258+
let out = anvil
259+
.eth_call_create(&dev, &encode_delegation_batch(addresses))
260+
.expect("the delegation batch must not revert");
261+
decode_delegation_batch(&out, addresses.len()).expect("decode failed")
262+
};
263+
264+
assert_eq!(
265+
read(&[bare, delegated, contract]),
266+
vec![
267+
Delegation::NotDelegated,
268+
Delegation::Delegated(delegate),
269+
Delegation::Other,
270+
]
271+
);
272+
assert_eq!(
273+
read(&[contract, bare, delegated]),
274+
vec![
275+
Delegation::Other,
276+
Delegation::NotDelegated,
277+
Delegation::Delegated(delegate),
278+
],
279+
"decoding is positional: the argument order decides, not the shape of the accounts"
280+
);
281+
}
282+
283+
#[test]
284+
fn should_read_a_full_batch_of_delegations_in_a_single_call() {
285+
let anvil = Anvil::start();
286+
let dev = address_from_hex(DEV_ACCOUNT);
287+
288+
let batch_of = |num_addresses: usize| -> Vec<DepositAddress> {
289+
(0..num_addresses as u64)
290+
.map(|index| DepositAddress::new(holder_at(index)))
291+
.collect()
292+
};
293+
294+
let delegate = Address::new([0xcd; 20]);
295+
let last = holder_at((MAX_CALLS_PER_BATCH - 1) as u64);
296+
anvil.set_code(&last, &delegation_designator(&delegate));
297+
298+
let full_batch = batch_of(MAX_CALLS_PER_BATCH);
299+
let out = anvil
300+
.eth_call_create(&dev, &encode_delegation_batch(&full_batch))
301+
.expect("a batch of MAX_CALLS_PER_BATCH addresses must stay within the node limits");
302+
let mut expected = vec![Delegation::NotDelegated; MAX_CALLS_PER_BATCH];
303+
*expected.last_mut().unwrap() = Delegation::Delegated(delegate);
304+
assert_eq!(
305+
decode_delegation_batch(&out, full_batch.len()).expect("decode failed"),
306+
expected
307+
);
308+
309+
const EIP_170_MAX_CODE_SIZE: usize = 24_576;
310+
const RETURNED_BYTES_PER_ADDRESS: usize = 32;
311+
const ONE_ADDRESS_PAST_THE_RETURNED_BLOB_CEILING: usize =
312+
EIP_170_MAX_CODE_SIZE / RETURNED_BYTES_PER_ADDRESS + 1;
313+
let error = anvil
314+
.eth_call_create(
315+
&dev,
316+
&encode_delegation_batch(&batch_of(ONE_ADDRESS_PAST_THE_RETURNED_BLOB_CEILING)),
317+
)
318+
.expect_err(
319+
"with one argument word per address the EIP-3860 initcode limit never binds; the \
320+
returned blob (one word per address) hits EIP-170 first, well above MAX_CALLS_PER_BATCH",
321+
);
322+
assert!(
323+
error.to_lowercase().contains("contractsizelimit"),
324+
"expected an EIP-170 code size error, got: {error}"
325+
);
326+
}
327+
246328
#[test]
247329
fn should_revert_the_whole_call_when_a_token_is_not_a_contract() {
248330
let anvil = Anvil::start();

rs/ethereum/cketh/test_utils/src/anvil.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ impl Anvil {
138138
}
139139

140140
/// Places `code` as the runtime bytecode at `address` (foundry's `anvil_setCode` cheatcode).
141-
pub(crate) fn set_code(&self, address: &Address, code: &[u8]) {
141+
pub fn set_code(&self, address: &Address, code: &[u8]) {
142142
self.rpc(
143143
"anvil_setCode",
144144
serde_json::json!([to_hex(address.as_ref()), to_hex(code)]),

0 commit comments

Comments
 (0)