Skip to content

Commit 9963414

Browse files
zjeskograndizzyklkvr
authored
fix: eip7702 cheatcodes multiple auth (#10623)
* fix: eip7702 cheatcodes multiple auth * fix: eip7702 cheatcodes nonce * Fmt and nonce fix * Active delegations as vec * Fix nonce for non senders and add test * Fix tests * Update crates/cheatcodes/src/inspector.rs Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com> * Nit: do not unwrap when looking for last delegation --------- Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: grandizzy <grandizzy.the.egg@gmail.com> Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
1 parent 01328a9 commit 9963414

File tree

5 files changed

+258
-27
lines changed

5 files changed

+258
-27
lines changed

crates/cheatcodes/src/inspector.rs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,10 @@ pub struct Cheatcodes {
371371
/// execution block environment.
372372
pub block: Option<BlockEnv>,
373373

374-
/// Currently active EIP-7702 delegation that will be consumed when building the next
374+
/// Currently active EIP-7702 delegations that will be consumed when building the next
375375
/// transaction. Set by `vm.attachDelegation()` and consumed via `.take()` during
376376
/// transaction construction.
377-
pub active_delegation: Option<SignedAuthorization>,
377+
pub active_delegations: Vec<SignedAuthorization>,
378378

379379
/// The active EIP-4844 blob that will be attached to the next call.
380380
pub active_blob_sidecar: Option<BlobTransactionSidecar>,
@@ -517,7 +517,7 @@ impl Cheatcodes {
517517
labels: config.labels.clone(),
518518
config,
519519
block: Default::default(),
520-
active_delegation: Default::default(),
520+
active_delegations: Default::default(),
521521
active_blob_sidecar: Default::default(),
522522
gas_price: Default::default(),
523523
pranks: Default::default(),
@@ -573,6 +573,11 @@ impl Cheatcodes {
573573
self.wallets = Some(wallets);
574574
}
575575

576+
/// Adds a delegation to the active delegations list.
577+
pub fn add_delegation(&mut self, authorization: SignedAuthorization) {
578+
self.active_delegations.push(authorization);
579+
}
580+
576581
/// Decodes the input data and applies the cheatcode.
577582
fn apply_cheatcode(
578583
&mut self,
@@ -1136,8 +1141,11 @@ impl Cheatcodes {
11361141
..Default::default()
11371142
};
11381143

1139-
match (self.active_delegation.take(), self.active_blob_sidecar.take()) {
1140-
(Some(_), Some(_)) => {
1144+
let active_delegations = std::mem::take(&mut self.active_delegations);
1145+
// Set active blob sidecar, if any.
1146+
if let Some(blob_sidecar) = self.active_blob_sidecar.take() {
1147+
// Ensure blob and delegation are not set for the same tx.
1148+
if !active_delegations.is_empty() {
11411149
let msg = "both delegation and blob are active; `attachBlob` and `attachDelegation` are not compatible";
11421150
return Some(CallOutcome {
11431151
result: InterpreterResult {
@@ -1148,21 +1156,22 @@ impl Cheatcodes {
11481156
memory_offset: call.return_memory_offset.clone(),
11491157
});
11501158
}
1151-
(Some(auth_list), None) => {
1152-
tx_req.authorization_list = Some(vec![auth_list]);
1153-
tx_req.sidecar = None;
1159+
tx_req.set_blob_sidecar(blob_sidecar);
1160+
}
11541161

1155-
// Increment nonce to reflect the signed authorization.
1156-
account.info.nonce += 1;
1157-
}
1158-
(None, Some(blob_sidecar)) => {
1159-
tx_req.set_blob_sidecar(blob_sidecar);
1160-
tx_req.authorization_list = None;
1161-
}
1162-
(None, None) => {
1163-
tx_req.sidecar = None;
1164-
tx_req.authorization_list = None;
1162+
// Apply active EIP-7702 delegations, if any.
1163+
if !active_delegations.is_empty() {
1164+
for auth in &active_delegations {
1165+
let Ok(authority) = auth.recover_authority() else {
1166+
continue;
1167+
};
1168+
if authority == broadcast.new_origin {
1169+
// Increment nonce of broadcasting account to reflect signed
1170+
// authorization.
1171+
account.info.nonce += 1;
1172+
}
11651173
}
1174+
tx_req.authorization_list = Some(active_delegations);
11661175
}
11671176

11681177
self.broadcastable_transactions.push_back(BroadcastableTransaction {

crates/cheatcodes/src/script.rs

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ fn attach_delegation(
113113
U256::from_be_bytes(s.0),
114114
);
115115
write_delegation(ccx, signed_auth.clone())?;
116-
ccx.state.active_delegation = Some(signed_auth);
116+
ccx.state.add_delegation(signed_auth);
117117
Ok(Default::default())
118118
}
119119

@@ -132,8 +132,13 @@ fn sign_delegation(
132132
nonce
133133
} else {
134134
let authority_acc = ccx.ecx.journaled_state.load_account(signer.address())?;
135-
// If we don't have a nonce then use next auth account nonce.
136-
authority_acc.data.info.nonce + 1
135+
// Calculate next nonce considering existing active delegations
136+
next_delegation_nonce(
137+
&ccx.state.active_delegations,
138+
signer.address(),
139+
&ccx.state.broadcast,
140+
authority_acc.data.info.nonce,
141+
)
137142
};
138143
let chain_id = if cross_chain { U256::from(0) } else { U256::from(ccx.ecx.cfg.chain_id) };
139144

@@ -143,7 +148,7 @@ fn sign_delegation(
143148
if attach {
144149
let signed_auth = SignedAuthorization::new_unchecked(auth, sig.v() as u8, sig.r(), sig.s());
145150
write_delegation(ccx, signed_auth.clone())?;
146-
ccx.state.active_delegation = Some(signed_auth);
151+
ccx.state.add_delegation(signed_auth);
147152
}
148153
Ok(SignedDelegation {
149154
v: sig.v() as u8,
@@ -155,11 +160,52 @@ fn sign_delegation(
155160
.abi_encode())
156161
}
157162

163+
/// Returns the next valid nonce for a delegation, considering existing active delegations.
164+
fn next_delegation_nonce(
165+
active_delegations: &[SignedAuthorization],
166+
authority: Address,
167+
broadcast: &Option<Broadcast>,
168+
account_nonce: u64,
169+
) -> u64 {
170+
match active_delegations
171+
.iter()
172+
.rfind(|auth| auth.recover_authority().is_ok_and(|recovered| recovered == authority))
173+
{
174+
Some(auth) => {
175+
// Increment nonce of last recorded delegation.
176+
auth.nonce + 1
177+
}
178+
None => {
179+
// First time a delegation is added for this authority.
180+
if let Some(broadcast) = broadcast {
181+
// Increment nonce if authority is the sender of transaction.
182+
if broadcast.new_origin == authority {
183+
return account_nonce + 1
184+
}
185+
}
186+
// Return current nonce if authority is not the sender of transaction.
187+
account_nonce
188+
}
189+
}
190+
}
191+
158192
fn write_delegation(ccx: &mut CheatsCtxt, auth: SignedAuthorization) -> Result<()> {
159193
let authority = auth.recover_authority().map_err(|e| format!("{e}"))?;
160194
let authority_acc = ccx.ecx.journaled_state.load_account(authority)?;
161-
if authority_acc.data.info.nonce + 1 != auth.nonce {
162-
return Err("invalid nonce".into());
195+
196+
let expected_nonce = next_delegation_nonce(
197+
&ccx.state.active_delegations,
198+
authority,
199+
&ccx.state.broadcast,
200+
authority_acc.data.info.nonce,
201+
);
202+
203+
if expected_nonce != auth.nonce {
204+
return Err(format!(
205+
"invalid nonce for {authority:?}: expected {expected_nonce}, got {}",
206+
auth.nonce
207+
)
208+
.into());
163209
}
164210

165211
if auth.address.is_zero() {

crates/forge/tests/cli/script.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2858,3 +2858,156 @@ ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
28582858
28592859
"#]]);
28602860
});
2861+
2862+
// Tests EIP-7702 with multiple auth <https://github.com/foundry-rs/foundry/issues/10551>
2863+
// Alice sends 5 ETH from Bob to Receiver1 and 1 ETH to Receiver2
2864+
forgetest_async!(can_broadcast_txes_with_multiple_auth, |prj, cmd| {
2865+
foundry_test_utils::util::initialize(prj.root());
2866+
prj.add_source(
2867+
"BatchCallDelegation.sol",
2868+
r#"
2869+
contract BatchCallDelegation {
2870+
event CallExecuted(address indexed to, uint256 indexed value, bytes data, bool success);
2871+
2872+
struct Call {
2873+
bytes data;
2874+
address to;
2875+
uint256 value;
2876+
}
2877+
2878+
function execute(Call[] calldata calls) external payable {
2879+
for (uint256 i = 0; i < calls.length; i++) {
2880+
Call memory call = calls[i];
2881+
(bool success,) = call.to.call{value: call.value}(call.data);
2882+
require(success, "call reverted");
2883+
emit CallExecuted(call.to, call.value, call.data, success);
2884+
}
2885+
}
2886+
}
2887+
"#,
2888+
)
2889+
.unwrap();
2890+
2891+
prj.add_script(
2892+
"BatchCallDelegationScript.s.sol",
2893+
r#"
2894+
import {Script, console} from "forge-std/Script.sol";
2895+
import {Vm} from "forge-std/Vm.sol";
2896+
import {BatchCallDelegation} from "../src/BatchCallDelegation.sol";
2897+
2898+
contract BatchCallDelegationScript is Script {
2899+
// Alice's address and private key (EOA with no initial contract code).
2900+
address payable ALICE_ADDRESS = payable(0x70997970C51812dc3A010C7d01b50e0d17dc79C8);
2901+
uint256 constant ALICE_PK = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;
2902+
2903+
// Bob's address and private key (Bob will execute transactions on Alice's behalf).
2904+
address constant BOB_ADDRESS = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
2905+
uint256 constant BOB_PK = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;
2906+
2907+
address constant RECEIVER_1 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
2908+
address constant RECEIVER_2 = 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc;
2909+
2910+
uint256 constant DEPLOYER_PK = 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6;
2911+
2912+
function run() public {
2913+
BatchCallDelegation.Call[] memory aliceCalls = new BatchCallDelegation.Call[](1);
2914+
aliceCalls[0] = BatchCallDelegation.Call({to: RECEIVER_1, value: 5 ether, data: ""});
2915+
2916+
BatchCallDelegation.Call[] memory bobCalls = new BatchCallDelegation.Call[](2);
2917+
bobCalls[0] = BatchCallDelegation.Call({to: RECEIVER_1, value: 5 ether, data: ""});
2918+
bobCalls[1] = BatchCallDelegation.Call({to: RECEIVER_2, value: 1 ether, data: ""});
2919+
2920+
vm.startBroadcast(DEPLOYER_PK);
2921+
BatchCallDelegation batcher = new BatchCallDelegation();
2922+
vm.stopBroadcast();
2923+
2924+
vm.startBroadcast(ALICE_PK);
2925+
vm.signAndAttachDelegation(address(batcher), ALICE_PK);
2926+
vm.signAndAttachDelegation(address(batcher), BOB_PK);
2927+
vm.signAndAttachDelegation(address(batcher), BOB_PK);
2928+
2929+
BatchCallDelegation(BOB_ADDRESS).execute(bobCalls);
2930+
2931+
vm.stopBroadcast();
2932+
}
2933+
}
2934+
"#,
2935+
)
2936+
.unwrap();
2937+
2938+
let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into()));
2939+
let (api, handle) = spawn(node_config).await;
2940+
2941+
cmd.args([
2942+
"script",
2943+
"script/BatchCallDelegationScript.s.sol",
2944+
"--rpc-url",
2945+
&handle.http_endpoint(),
2946+
"--non-interactive",
2947+
"--slow",
2948+
"--broadcast",
2949+
"--evm-version",
2950+
"prague",
2951+
])
2952+
.assert_success()
2953+
.stdout_eq(str![[r#"
2954+
[COMPILING_FILES] with [SOLC_VERSION]
2955+
[SOLC_VERSION] [ELAPSED]
2956+
Compiler run successful!
2957+
Script ran successfully.
2958+
2959+
## Setting up 1 EVM.
2960+
2961+
==========================
2962+
2963+
Chain 31337
2964+
2965+
[ESTIMATED_GAS_PRICE]
2966+
2967+
[ESTIMATED_TOTAL_GAS_USED]
2968+
2969+
[ESTIMATED_AMOUNT_REQUIRED]
2970+
2971+
==========================
2972+
2973+
2974+
==========================
2975+
2976+
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
2977+
2978+
[SAVED_TRANSACTIONS]
2979+
2980+
[SAVED_SENSITIVE_VALUES]
2981+
2982+
2983+
"#]]);
2984+
2985+
// Alice nonce should be 2 (tx sender and one auth)
2986+
let alice_acc = api
2987+
.get_account(address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), None)
2988+
.await
2989+
.unwrap();
2990+
assert_eq!(alice_acc.nonce, 2);
2991+
2992+
// Bob nonce should be 2 (two auths) and balance reduced by 6 ETH.
2993+
let bob_acc = api
2994+
.get_account(address!("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), None)
2995+
.await
2996+
.unwrap();
2997+
assert_eq!(bob_acc.nonce, 2);
2998+
assert_eq!(bob_acc.balance.to_string(), "94000000000000000000");
2999+
3000+
// Receiver balances should be updated with 5 ETH and 1 ETH.
3001+
let receiver1 = api
3002+
.get_account(address!("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"), None)
3003+
.await
3004+
.unwrap();
3005+
assert_eq!(receiver1.nonce, 0);
3006+
assert_eq!(receiver1.balance.to_string(), "105000000000000000000");
3007+
let receiver2 = api
3008+
.get_account(address!("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"), None)
3009+
.await
3010+
.unwrap();
3011+
assert_eq!(receiver2.nonce, 0);
3012+
assert_eq!(receiver2.balance.to_string(), "101000000000000000000");
3013+
});

crates/forge/tests/cli/test_cmd.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3306,9 +3306,9 @@ Traces:
33063306
├─ [0] VM::label(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], "alice")
33073307
│ └─ ← [Return]
33083308
├─ [0] VM::signDelegation(0x0000000000000000000000000000000000000000, "<pk>")
3309-
│ └─ ← [Return] (0, 0x38db2a0ada75402af7cd5bdb8248a1a5b4fec65fdafea4f935084f00dc2ff3c5, 0x29ce7b1c82f9ceaec21f12d690ba8fe6ecba65869caf6ab2d85d79890dc42df2, 1, 0x0000000000000000000000000000000000000000)
3309+
│ └─ ← [Return] (0, 0x3d6ad67cc3dc94101a049f85f96937513a05485ae0f8b27545d25c4f71b12cf9, 0x3c0f2d62834f59d6ef0209e8a935f80a891a236eb18ac0e3700dd8f7ac8ae279, 0, 0x0000000000000000000000000000000000000000)
33103310
├─ [0] VM::signAndAttachDelegation(0x0000000000000000000000000000000000000000, "<pk>")
3311-
│ └─ ← [Return] (0, 0x38db2a0ada75402af7cd5bdb8248a1a5b4fec65fdafea4f935084f00dc2ff3c5, 0x29ce7b1c82f9ceaec21f12d690ba8fe6ecba65869caf6ab2d85d79890dc42df2, 1, 0x0000000000000000000000000000000000000000)
3311+
│ └─ ← [Return] (0, 0x3d6ad67cc3dc94101a049f85f96937513a05485ae0f8b27545d25c4f71b12cf9, 0x3c0f2d62834f59d6ef0209e8a935f80a891a236eb18ac0e3700dd8f7ac8ae279, 0, 0x0000000000000000000000000000000000000000)
33123312
└─ ← [Stop]
33133313
...
33143314

testdata/default/cheats/AttachDelegation.t.sol

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ contract AttachDelegationTest is DSTest {
6262
vm._expectCheatcodeRevert("vm.attachDelegation: invalid nonce");
6363
vm.attachDelegation(signedDelegation);
6464

65-
signedDelegation = vm.signDelegation(address(implementation), alice_pk, 1);
65+
signedDelegation = vm.signDelegation(address(implementation), alice_pk, 0);
6666
vm.attachDelegation(signedDelegation);
6767
}
6868

@@ -192,7 +192,30 @@ contract AttachDelegationTest is DSTest {
192192
vm._expectCheatcodeRevert("vm.signAndAttachDelegation: invalid nonce");
193193
vm.signAndAttachDelegation(address(implementation), alice_pk, 11);
194194

195+
vm.signAndAttachDelegation(address(implementation), alice_pk, 0);
196+
}
197+
198+
function testMultipleDelegationsOnTransaction() public {
199+
vm.signAndAttachDelegation(address(implementation), alice_pk);
200+
vm.signAndAttachDelegation(address(implementation2), bob_pk);
201+
SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](2);
202+
calls[0] = SimpleDelegateContract.Call({
203+
to: address(token),
204+
data: abi.encodeCall(ERC20.mint, (50, address(this))),
205+
value: 0
206+
});
207+
calls[1] =
208+
SimpleDelegateContract.Call({to: address(token), data: abi.encodeCall(ERC20.mint, (50, alice)), value: 0});
209+
vm.broadcast(bob_pk);
210+
SimpleDelegateContract(alice).execute(calls);
211+
212+
assertEq(token.balanceOf(address(this)), 50);
213+
assertEq(token.balanceOf(alice), 50);
214+
215+
vm._expectCheatcodeRevert("vm.signAndAttachDelegation: invalid nonce");
195216
vm.signAndAttachDelegation(address(implementation), alice_pk, 1);
217+
vm.signAndAttachDelegation(address(implementation), alice_pk, 0);
218+
vm.signAndAttachDelegation(address(implementation2), bob_pk, 2);
196219
}
197220
}
198221

0 commit comments

Comments
 (0)