Skip to content

Commit 8906af0

Browse files
evchipklkvrgrandizzy
authored andcommitted
Support EIP-7702 Delegations in Forge (foundry-rs#9236)
* add EIP-7702 cheatcodes: createDelegation, signDelegation, attachDelegation * add cheatcode implementations for EIP-7702: createDelegationCall, signDelegationCall, attachDelegationCall; modify broadcast to check if sender has a delegation * add delegations hashmap to Cheatcodes struct * add revm crate * create AttachDelegationTest for EIP-7702 transactions * regen cheatcodes.json * cargo fmt * move broadcast under attachDelegation * combine createDelegationCall logic with signDelegationCall in order to create and sign delegation in a single call; remove delegation logic from broadcast() - no need to track delegations here * remove revm import from workspace * combine createDelegation logic inton signDelegation for simplicity * remove revm from forge script deps * combine createDelegation with signDelegation * WIP - refactor test to use SimpleDelegateContract and ERC20 - test currently failing bc 7702 implementation.execute not executed as Alice EOA * add logic to include authorization_list for EIP 7702 in TransactionRequest by searching delegations hash map by active_delegation * add address authority param to attachDelegation; remove nonce param from signDelegation, as it can be constructed in cheatcode. * remove 7702 tx request construction logic - now handled in attachDelegation cheatcode implementation * refactor attachDelegation cheatcode implementation to handle verifying signature and setting bytecode on EOA; refactor signDelegation cheatcode implementation to get nonce from signer * remove nonce param from attachDelegation cheatcode in favor of loading from authority account * refactor test to check for code on alice account and call execute on alice account through SimpleDelegateContract * revert refactor on TransactionRequest * format * cargo fmt * fix clippy errors * remove faulty logic comparing nonce to itself - nonce still checked by recovered signature * add more tests to cover revert cases on attachDelegation and multiple calls via delegation contract * cargo fmt * restore logic to check if there's an active delegation when building TransactionRequest; add fixed values for gas and max_priority_fee_per_gas to ensure tx success, with TODO comment to explain what's left * remove obsolete comment * add comments explaining delegations and active_delegation * cargo fmt * add logic to increase gas limit by PER_EMPTY_ACCOUNT_COST(25k) if tx includes authorization list for EIP 7702 tx, which is seemingly not accounted for in gas estimation; remove hardcoded gas values from call_with_executor * revert logic to add PER_EMPTY_ACCOUNT_COST for EIP 7702 txs - handled inside of revm now * remove manually setting transaction type to 4 if auth list is present - handled in revm * add method set_delegation to Executor for setting EIP-7702 authorization list in the transaction environment; call set_delegation from simulate_and_fill if auth list is not empty * remove redundancy with TransactionMaybeSigned var tx * cargo fmt * refactor: use authorization_list() helper to return authorization_list and set delegation * refactor: change Cheatcodes::active_delegation to Option<SignedAuthorization> and remove delegations hashmap - tx will only use one active delegation at a time, so no need for mapping * replace verbose logic to set bytecode on EOA with journaled_state.set_code helper * cargo fmt * increment nonce of authority account * add logic to set authorization_list to None if active_delegation is None * add test testSwitchDelegation to assert that attaching an additional delegation switches the implementation on the EOA * remove set_delegation logic in favor of adding call_raw_with_authorization - previous approach kept the delegation in the TxEnv, resulting in higher gas cost for all subsequent calls after the delegation was applied * refactor signDelegation to return struct SignedDelegation and for attachDelegation to accept SignedDelegation * update delegation tests to reflect change in cheatcode interface for signDelegation and attachDelegation * add cheatcode signAndAttachDelegation * add signAndAttachDelegationCall cheatcode logic; refactor helper methods for shared logic used in 7702 delegation cheatcodes * add test testCallSingleSignAndAttachDelegation for new cheatcode signAndAttachDelegation * add comments to SignedDelegation struct and cargo fmt * cargo fmt * fix ci * fix spec --------- Co-authored-by: Arsenii Kulikov <[email protected]> Co-authored-by: grandizzy <[email protected]>
1 parent 93a2bd9 commit 8906af0

File tree

13 files changed

+477
-18
lines changed

13 files changed

+477
-18
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ impl Cheatcodes<'static> {
8787
Vm::Gas::STRUCT.clone(),
8888
Vm::DebugStep::STRUCT.clone(),
8989
Vm::BroadcastTxSummary::STRUCT.clone(),
90+
Vm::SignedDelegation::STRUCT.clone(),
9091
]),
9192
enums: Cow::Owned(vec![
9293
Vm::CallerMode::ENUM.clone(),

crates/cheatcodes/spec/src/vm.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,22 @@ interface Vm {
308308
bool success;
309309
}
310310

311+
/// Holds a signed EIP-7702 authorization for an authority account to delegate to an implementation.
312+
struct SignedDelegation {
313+
/// The y-parity of the recovered secp256k1 signature (0 or 1).
314+
uint8 v;
315+
/// First 32 bytes of the signature.
316+
bytes32 r;
317+
/// Second 32 bytes of the signature.
318+
bytes32 s;
319+
/// The current nonce of the authority account at signing time.
320+
/// Used to ensure signature can't be replayed after account nonce changes.
321+
uint64 nonce;
322+
/// Address of the contract implementation that will be delegated to.
323+
/// Gets encoded into delegation code: 0xef0100 || implementation.
324+
address implementation;
325+
}
326+
311327
// ======== EVM ========
312328

313329
/// Gets the address for a given private key.
@@ -2018,6 +2034,18 @@ interface Vm {
20182034
#[cheatcode(group = Scripting)]
20192035
function broadcastRawTransaction(bytes calldata data) external;
20202036

2037+
/// Sign an EIP-7702 authorization for delegation
2038+
#[cheatcode(group = Scripting)]
2039+
function signDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation);
2040+
2041+
/// Designate the next call as an EIP-7702 transaction
2042+
#[cheatcode(group = Scripting)]
2043+
function attachDelegation(SignedDelegation memory signedDelegation) external;
2044+
2045+
/// Sign an EIP-7702 authorization and designate the next call as an EIP-7702 transaction
2046+
#[cheatcode(group = Scripting)]
2047+
function signAndAttachDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation);
2048+
20212049
/// Returns addresses of available unlocked wallets in the script environment.
20222050
#[cheatcode(group = Scripting)]
20232051
function getWallets() external returns (address[] memory wallets);

crates/cheatcodes/src/inspector.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ use revm::{
4646
EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction,
4747
InterpreterResult,
4848
},
49-
primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, EOF_MAGIC_BYTES},
49+
primitives::{
50+
BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId,
51+
EOF_MAGIC_BYTES,
52+
},
5053
EvmContext, InnerEvmContext, Inspector,
5154
};
5255
use serde_json::Value;
@@ -373,6 +376,11 @@ pub struct Cheatcodes {
373376
/// execution block environment.
374377
pub block: Option<BlockEnv>,
375378

379+
/// Currently active EIP-7702 delegation that will be consumed when building the next
380+
/// transaction. Set by `vm.attachDelegation()` and consumed via `.take()` during
381+
/// transaction construction.
382+
pub active_delegation: Option<SignedAuthorization>,
383+
376384
/// The gas price.
377385
///
378386
/// Used in the cheatcode handler to overwrite the gas price separately from the gas price
@@ -497,6 +505,7 @@ impl Cheatcodes {
497505
labels: config.labels.clone(),
498506
config,
499507
block: Default::default(),
508+
active_delegation: Default::default(),
500509
gas_price: Default::default(),
501510
prank: Default::default(),
502511
expected_revert: Default::default(),
@@ -1014,18 +1023,26 @@ where {
10141023
let account =
10151024
ecx.journaled_state.state().get_mut(&broadcast.new_origin).unwrap();
10161025

1026+
let mut tx_req = TransactionRequest {
1027+
from: Some(broadcast.new_origin),
1028+
to: Some(TxKind::from(Some(call.target_address))),
1029+
value: call.transfer_value(),
1030+
input: TransactionInput::new(call.input.clone()),
1031+
nonce: Some(account.info.nonce),
1032+
chain_id: Some(ecx.env.cfg.chain_id),
1033+
gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None },
1034+
..Default::default()
1035+
};
1036+
1037+
if let Some(auth_list) = self.active_delegation.take() {
1038+
tx_req.authorization_list = Some(vec![auth_list]);
1039+
} else {
1040+
tx_req.authorization_list = None;
1041+
}
1042+
10171043
self.broadcastable_transactions.push_back(BroadcastableTransaction {
10181044
rpc: ecx.db.active_fork_url(),
1019-
transaction: TransactionRequest {
1020-
from: Some(broadcast.new_origin),
1021-
to: Some(TxKind::from(Some(call.target_address))),
1022-
value: call.transfer_value(),
1023-
input: TransactionInput::new(call.input.clone()),
1024-
nonce: Some(account.info.nonce),
1025-
gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None },
1026-
..Default::default()
1027-
}
1028-
.into(),
1045+
transaction: tx_req.into(),
10291046
});
10301047
debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call");
10311048

crates/cheatcodes/src/script.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
33
use crate::{Cheatcode, CheatsCtxt, Result, Vm::*};
44
use alloy_primitives::{Address, B256, U256};
5+
use alloy_rpc_types::Authorization;
6+
use alloy_signer::{Signature, SignerSync};
57
use alloy_signer_local::PrivateKeySigner;
68
use alloy_sol_types::SolValue;
79
use foundry_wallets::{multi_wallet::MultiWallet, WalletSigner};
810
use parking_lot::Mutex;
11+
use revm::primitives::{Bytecode, SignedAuthorization};
912
use std::sync::Arc;
1013

1114
impl Cheatcode for broadcast_0Call {
@@ -29,6 +32,92 @@ impl Cheatcode for broadcast_2Call {
2932
}
3033
}
3134

35+
impl Cheatcode for attachDelegationCall {
36+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
37+
let Self { signedDelegation } = self;
38+
let SignedDelegation { v, r, s, nonce, implementation } = signedDelegation;
39+
40+
let auth = Authorization {
41+
address: *implementation,
42+
nonce: *nonce,
43+
chain_id: ccx.ecx.env.cfg.chain_id,
44+
};
45+
let signed_auth = SignedAuthorization::new_unchecked(
46+
auth,
47+
*v,
48+
U256::from_be_bytes(r.0),
49+
U256::from_be_bytes(s.0),
50+
);
51+
write_delegation(ccx, signed_auth.clone())?;
52+
ccx.state.active_delegation = Some(signed_auth);
53+
Ok(Default::default())
54+
}
55+
}
56+
57+
impl Cheatcode for signDelegationCall {
58+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
59+
let Self { implementation, privateKey } = self;
60+
let signer = PrivateKeySigner::from_bytes(&B256::from(*privateKey))?;
61+
let authority = signer.address();
62+
let (auth, nonce) = create_auth(ccx, *implementation, authority)?;
63+
let sig = signer.sign_hash_sync(&auth.signature_hash())?;
64+
Ok(sig_to_delegation(sig, nonce, *implementation).abi_encode())
65+
}
66+
}
67+
68+
impl Cheatcode for signAndAttachDelegationCall {
69+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
70+
let Self { implementation, privateKey } = self;
71+
let signer = PrivateKeySigner::from_bytes(&B256::from(*privateKey))?;
72+
let authority = signer.address();
73+
let (auth, nonce) = create_auth(ccx, *implementation, authority)?;
74+
let sig = signer.sign_hash_sync(&auth.signature_hash())?;
75+
let signed_auth = sig_to_auth(sig, auth);
76+
write_delegation(ccx, signed_auth.clone())?;
77+
ccx.state.active_delegation = Some(signed_auth);
78+
Ok(sig_to_delegation(sig, nonce, *implementation).abi_encode())
79+
}
80+
}
81+
82+
fn create_auth(
83+
ccx: &mut CheatsCtxt,
84+
implementation: Address,
85+
authority: Address,
86+
) -> Result<(Authorization, u64)> {
87+
let authority_acc = ccx.ecx.journaled_state.load_account(authority, &mut ccx.ecx.db)?;
88+
let nonce = authority_acc.data.info.nonce;
89+
Ok((
90+
Authorization { address: implementation, nonce, chain_id: ccx.ecx.env.cfg.chain_id },
91+
nonce,
92+
))
93+
}
94+
95+
fn write_delegation(ccx: &mut CheatsCtxt, auth: SignedAuthorization) -> Result<()> {
96+
let authority = auth.recover_authority().map_err(|e| format!("{e}"))?;
97+
let authority_acc = ccx.ecx.journaled_state.load_account(authority, &mut ccx.ecx.db)?;
98+
if authority_acc.data.info.nonce != auth.nonce {
99+
return Err("invalid nonce".into());
100+
}
101+
authority_acc.data.info.nonce += 1;
102+
let bytecode = Bytecode::new_eip7702(*auth.address());
103+
ccx.ecx.journaled_state.set_code(authority, bytecode);
104+
Ok(())
105+
}
106+
107+
fn sig_to_delegation(sig: Signature, nonce: u64, implementation: Address) -> SignedDelegation {
108+
SignedDelegation {
109+
v: sig.v().y_parity() as u8,
110+
r: sig.r().into(),
111+
s: sig.s().into(),
112+
nonce,
113+
implementation,
114+
}
115+
}
116+
117+
fn sig_to_auth(sig: Signature, auth: Authorization) -> SignedAuthorization {
118+
SignedAuthorization::new_unchecked(auth, sig.v().y_parity() as u8, sig.r(), sig.s())
119+
}
120+
32121
impl Cheatcode for startBroadcast_0Call {
33122
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
34123
let Self {} = self;

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ foundry-config.workspace = true
2020

2121
alloy-contract.workspace = true
2222
alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] }
23+
alloy-eips.workspace = true
2324
alloy-json-abi.workspace = true
2425
alloy-json-rpc.workspace = true
2526
alloy-primitives = { workspace = true, features = [

crates/common/src/transactions.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Wrappers for transactions.
22
33
use alloy_consensus::{Transaction, TxEnvelope};
4+
use alloy_eips::eip7702::SignedAuthorization;
45
use alloy_primitives::{Address, TxKind, U256};
56
use alloy_provider::{
67
network::{AnyNetwork, ReceiptResponse, TransactionBuilder},
@@ -226,6 +227,14 @@ impl TransactionMaybeSigned {
226227
Self::Unsigned(tx) => tx.nonce,
227228
}
228229
}
230+
231+
pub fn authorization_list(&self) -> Option<Vec<SignedAuthorization>> {
232+
match self {
233+
Self::Signed { tx, .. } => tx.authorization_list().map(|auths| auths.to_vec()),
234+
Self::Unsigned(tx) => tx.authorization_list.as_deref().map(|auths| auths.to_vec()),
235+
}
236+
.filter(|auths| !auths.is_empty())
237+
}
229238
}
230239

231240
impl From<TransactionRequest> for TransactionMaybeSigned {

0 commit comments

Comments
 (0)