Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support EIP-7702 Delegations in Forge #9236

Merged
merged 57 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2467a3f
add EIP-7702 cheatcodes: createDelegation, signDelegation, attachDele…
evchip Oct 30, 2024
b16edd8
add cheatcode implementations for EIP-7702: createDelegationCall, sig…
evchip Oct 30, 2024
a286c7d
add delegations hashmap to Cheatcodes struct
evchip Oct 30, 2024
a6de137
add revm crate
evchip Oct 30, 2024
619aa1e
create AttachDelegationTest for EIP-7702 transactions
evchip Oct 30, 2024
d5ae337
regen cheatcodes.json
evchip Oct 30, 2024
05cdad8
cargo fmt
evchip Oct 31, 2024
1d8f42e
move broadcast under attachDelegation
evchip Oct 31, 2024
c819383
Merge branch 'master' into feature/forge-eip-7702
evchip Oct 31, 2024
da62cc8
combine createDelegationCall logic with signDelegationCall in order t…
evchip Nov 3, 2024
bad6230
remove revm import from workspace
evchip Nov 3, 2024
b3666aa
combine createDelegation logic inton signDelegation for simplicity
evchip Nov 3, 2024
376a092
remove revm from forge script deps
evchip Nov 3, 2024
93963ee
combine createDelegation with signDelegation
evchip Nov 3, 2024
df33332
WIP - refactor test to use SimpleDelegateContract and ERC20 - test cu…
evchip Nov 3, 2024
912ce6d
add logic to include authorization_list for EIP 7702 in TransactionRe…
evchip Nov 3, 2024
e97e185
add address authority param to attachDelegation; remove nonce param f…
evchip Nov 4, 2024
40fd5f1
remove 7702 tx request construction logic - now handled in attachDele…
evchip Nov 4, 2024
bdef83c
refactor attachDelegation cheatcode implementation to handle verifyin…
evchip Nov 4, 2024
e4082b1
remove nonce param from attachDelegation cheatcode in favor of loadin…
evchip Nov 4, 2024
f18a0e8
refactor test to check for code on alice account and call execute on …
evchip Nov 4, 2024
d8ea3ec
revert refactor on TransactionRequest
evchip Nov 4, 2024
0bd6f42
format
evchip Nov 4, 2024
8f480fe
cargo fmt
evchip Nov 5, 2024
25aeea7
fix clippy errors
evchip Nov 5, 2024
4310c2d
remove faulty logic comparing nonce to itself - nonce still checked b…
evchip Nov 6, 2024
bdc0b78
add more tests to cover revert cases on attachDelegation and multiple…
evchip Nov 6, 2024
c10be3e
cargo fmt
evchip Nov 6, 2024
7090999
restore logic to check if there's an active delegation when building …
evchip Nov 7, 2024
af7e39c
remove obsolete comment
evchip Nov 7, 2024
4394065
add comments explaining delegations and active_delegation
evchip Nov 7, 2024
aa0ece7
cargo fmt
evchip Nov 7, 2024
a833dd6
add logic to increase gas limit by PER_EMPTY_ACCOUNT_COST(25k) if tx …
evchip Nov 8, 2024
f432430
revert logic to add PER_EMPTY_ACCOUNT_COST for EIP 7702 txs - handled…
evchip Nov 10, 2024
6d46417
remove manually setting transaction type to 4 if auth list is present…
evchip Nov 10, 2024
d79f376
add method set_delegation to Executor for setting EIP-7702 authorizat…
evchip Nov 10, 2024
edbaf4c
Merge branch 'master' into feature/forge-eip-7702
evchip Nov 10, 2024
c050393
remove redundancy with TransactionMaybeSigned var tx
evchip Nov 10, 2024
cdfe503
cargo fmt
evchip Nov 10, 2024
475216d
refactor: use authorization_list() helper to return authorization_lis…
evchip Nov 11, 2024
d0ad8bd
refactor: change Cheatcodes::active_delegation to Option<SignedAuthor…
evchip Nov 11, 2024
113854c
replace verbose logic to set bytecode on EOA with journaled_state.set…
evchip Nov 12, 2024
0a93989
cargo fmt
evchip Nov 12, 2024
74302de
increment nonce of authority account
evchip Nov 12, 2024
de9dea4
add logic to set authorization_list to None if active_delegation is None
evchip Nov 12, 2024
d560464
add test testSwitchDelegation to assert that attaching an additional …
evchip Nov 12, 2024
2a30b42
remove set_delegation logic in favor of adding call_raw_with_authoriz…
evchip Nov 14, 2024
713bd64
refactor signDelegation to return struct SignedDelegation and for att…
evchip Nov 18, 2024
89cd62d
update delegation tests to reflect change in cheatcode interface for …
evchip Nov 18, 2024
7df7dcb
add cheatcode signAndAttachDelegation
evchip Nov 18, 2024
5151459
add signAndAttachDelegationCall cheatcode logic; refactor helper meth…
evchip Nov 18, 2024
1c6e7d0
add test testCallSingleSignAndAttachDelegation for new cheatcode sign…
evchip Nov 18, 2024
ff2801c
add comments to SignedDelegation struct and cargo fmt
evchip Nov 18, 2024
002221a
cargo fmt
evchip Nov 19, 2024
e829b6e
fix ci
klkvr Nov 19, 2024
d2d2376
fix spec
klkvr Nov 19, 2024
8fa8e64
Merge branch 'master' into feature/forge-eip-7702
grandizzy Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2002,6 +2002,14 @@ interface Vm {
#[cheatcode(group = Scripting)]
function broadcastRawTransaction(bytes calldata data) external;

/// Sign an EIP-7702 authorization for delegation
#[cheatcode(group = Scripting)]
function signDelegation(address implementation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);

/// Designate the next call as an EIP-7702 transaction
#[cheatcode(group = Scripting)]
function attachDelegation(address implementation, address authority, uint8 v, bytes32 r, bytes32 s) external;

/// Returns addresses of available unlocked wallets in the script environment.
#[cheatcode(group = Scripting)]
function getWallets() external returns (address[] memory wallets);
Expand Down
39 changes: 28 additions & 11 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ use revm::{
EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction,
InterpreterResult,
},
primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, EOF_MAGIC_BYTES},
primitives::{
BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId,
EOF_MAGIC_BYTES,
},
EvmContext, InnerEvmContext, Inspector,
};
use serde_json::Value;
Expand Down Expand Up @@ -373,6 +376,11 @@ pub struct Cheatcodes {
/// execution block environment.
pub block: Option<BlockEnv>,

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

/// The gas price.
///
/// Used in the cheatcode handler to overwrite the gas price separately from the gas price
Expand Down Expand Up @@ -497,6 +505,7 @@ impl Cheatcodes {
labels: config.labels.clone(),
config,
block: Default::default(),
active_delegation: Default::default(),
gas_price: Default::default(),
prank: Default::default(),
expected_revert: Default::default(),
Expand Down Expand Up @@ -1001,18 +1010,26 @@ where {
let account =
ecx.journaled_state.state().get_mut(&broadcast.new_origin).unwrap();

let mut tx_req = TransactionRequest {
from: Some(broadcast.new_origin),
to: Some(TxKind::from(Some(call.target_address))),
value: call.transfer_value(),
input: TransactionInput::new(call.input.clone()),
nonce: Some(account.info.nonce),
chain_id: Some(ecx.env.cfg.chain_id),
gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None },
..Default::default()
};

if let Some(auth_list) = self.active_delegation.take() {
tx_req.authorization_list = Some(vec![auth_list]);
} else {
tx_req.authorization_list = None;
}

self.broadcastable_transactions.push_back(BroadcastableTransaction {
rpc: ecx.db.active_fork_url(),
transaction: TransactionRequest {
from: Some(broadcast.new_origin),
to: Some(TxKind::from(Some(call.target_address))),
value: call.transfer_value(),
input: TransactionInput::new(call.input.clone()),
nonce: Some(account.info.nonce),
gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None },
..Default::default()
}
.into(),
transaction: tx_req.into(),
});
debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call");

Expand Down
62 changes: 62 additions & 0 deletions crates/cheatcodes/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

use crate::{Cheatcode, CheatsCtxt, Result, Vm::*};
use alloy_primitives::{Address, B256, U256};
use alloy_rpc_types::Authorization;
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::SolValue;
use foundry_wallets::{multi_wallet::MultiWallet, WalletSigner};
use parking_lot::Mutex;
use revm::primitives::{Bytecode, SignedAuthorization};
use std::sync::Arc;

impl Cheatcode for broadcast_0Call {
Expand All @@ -29,6 +32,65 @@ impl Cheatcode for broadcast_2Call {
}
}

impl Cheatcode for attachDelegationCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { implementation, authority, v, r, s } = self;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove authority argument from here. it's redundant as we are always recovering it from signature

also wdyt on adding signAndAttach method? I think common flow would be calling both of the cheatcodes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing how to construct the authorization without the authority's nonce, which we'd need to recover the authority from the signature. Can you clarify the flow you're thinking of?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can get the nonce from state

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but to get the nonce from state we need the authority's address to load their account - am I missing some other way to get their nonce?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in signDelegation we can get authority address from private key, then get nonce, construct and sign delegation

and in attachDelegation we can just recover authority from the signed delegation. right now the passed authority address is anyway expected to be the same as recovered one making it redundant

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see what you mean. didn't notice that attachDelegation doesn't accept nonce

let's make signDelegation return a new struct SignedDelegation which will contain v,r,s,nonce,implementation. And then attachDelegation would just accept this object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented a new cheatcode signAndAttachDelegation and refactored the existing cheatcodes to use struct SignedDelegation. Let me know what you think. Thanks!


let authority_acc = ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?;

let auth = Authorization {
address: *implementation,
nonce: authority_acc.data.info.nonce,
chain_id: ccx.ecx.env.cfg.chain_id,
};
let signed_auth = SignedAuthorization::new_unchecked(
auth,
*v,
U256::from_be_bytes(r.0),
U256::from_be_bytes(s.0),
);

// verify signature is from claimed authority
let recovered = signed_auth.recover_authority().map_err(|e| format!("{e}"))?;
if recovered != *authority {
return Err("invalid signature".into());
}

authority_acc.data.info.nonce += 1;
let bytecode = Bytecode::new_eip7702(*implementation);
ccx.ecx.journaled_state.set_code(*authority, bytecode);

ccx.state.active_delegation = Some(signed_auth);

Ok(Default::default())
}
}

impl Cheatcode for signDelegationCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { implementation, privateKey } = self;
let key_bytes = B256::from(*privateKey);
let signer: alloy_signer_local::LocalSigner<ecdsa::SigningKey<k256::Secp256k1>> =
PrivateKeySigner::from_bytes(&key_bytes)?;
let authority = signer.address();
let nonce =
ccx.ecx.journaled_state.load_account(authority, &mut ccx.ecx.db)?.data.info.nonce;
let auth =
Authorization { address: *implementation, nonce, chain_id: ccx.ecx.env.cfg.chain_id };
let hash = auth.signature_hash();
let signer = super::crypto::parse_wallet(privateKey)?;
let sig = signer.sign_hash_sync(&hash)?;
Ok(encode_delegation_sig(sig))
}
}

fn encode_delegation_sig(sig: alloy_primitives::Signature) -> Vec<u8> {
let v = U256::from(sig.v().y_parity() as u8);
let r = sig.r();
let s = sig.s();
(v, r, s).abi_encode()
}

impl Cheatcode for startBroadcast_0Call {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self {} = self;
Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ foundry-config.workspace = true

alloy-contract.workspace = true
alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] }
alloy-eips.workspace = true
alloy-json-abi.workspace = true
alloy-json-rpc.workspace = true
alloy-primitives = { workspace = true, features = [
Expand Down
9 changes: 9 additions & 0 deletions crates/common/src/transactions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Wrappers for transactions.

use alloy_consensus::{Transaction, TxEnvelope};
use alloy_eips::eip7702::SignedAuthorization;
use alloy_primitives::{Address, TxKind, U256};
use alloy_provider::{
network::{AnyNetwork, ReceiptResponse, TransactionBuilder},
Expand Down Expand Up @@ -226,6 +227,14 @@ impl TransactionMaybeSigned {
Self::Unsigned(tx) => tx.nonce,
}
}

pub fn authorization_list(&self) -> Option<Vec<SignedAuthorization>> {
match self {
Self::Signed { tx, .. } => tx.authorization_list().map(|auths| auths.to_vec()),
Self::Unsigned(tx) => tx.authorization_list.as_deref().map(|auths| auths.to_vec()),
}
.filter(|auths| !auths.is_empty())
}
}

impl From<TransactionRequest> for TransactionMaybeSigned {
Expand Down
19 changes: 17 additions & 2 deletions crates/evm/evm/src/executors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ use revm::{
db::{DatabaseCommit, DatabaseRef},
interpreter::{return_ok, InstructionResult},
primitives::{
BlockEnv, Bytecode, Env, EnvWithHandlerCfg, ExecutionResult, Output, ResultAndState,
SpecId, TxEnv, TxKind,
AuthorizationList, BlockEnv, Bytecode, Env, EnvWithHandlerCfg, ExecutionResult, Output,
ResultAndState, SignedAuthorization, SpecId, TxEnv, TxKind,
},
};
use std::borrow::Cow;
Expand Down Expand Up @@ -378,6 +378,21 @@ impl Executor {
self.call_with_env(env)
}

/// Performs a raw call to an account on the current state of the VM with an EIP-7702
/// authorization list.
pub fn call_raw_with_authorization(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
value: U256,
authorization_list: Vec<SignedAuthorization>,
) -> eyre::Result<RawCallResult> {
let mut env = self.build_test_env(from, to.into(), calldata, value);
env.tx.authorization_list = Some(AuthorizationList::Signed(authorization_list));
self.call_with_env(env)
}

/// Performs a raw call to an account on the current state of the VM.
pub fn transact_raw(
&mut self,
Expand Down
26 changes: 23 additions & 3 deletions crates/script/src/runner.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::ScriptResult;
use crate::build::ScriptPredeployLibraries;
use alloy_eips::eip7702::SignedAuthorization;
use alloy_primitives::{Address, Bytes, TxKind, U256};
use alloy_rpc_types::TransactionRequest;
use eyre::Result;
Expand Down Expand Up @@ -223,7 +224,7 @@ impl ScriptRunner {

/// Executes the method that will collect all broadcastable transactions.
pub fn script(&mut self, address: Address, calldata: Bytes) -> Result<ScriptResult> {
self.call(self.evm_opts.sender, address, calldata, U256::ZERO, false)
self.call(self.evm_opts.sender, address, calldata, U256::ZERO, None, false)
}

/// Runs a broadcastable transaction locally and persists its state.
Expand All @@ -233,9 +234,17 @@ impl ScriptRunner {
to: Option<Address>,
calldata: Option<Bytes>,
value: Option<U256>,
authorization_list: Option<Vec<SignedAuthorization>>,
) -> Result<ScriptResult> {
if let Some(to) = to {
self.call(from, to, calldata.unwrap_or_default(), value.unwrap_or(U256::ZERO), true)
self.call(
from,
to,
calldata.unwrap_or_default(),
value.unwrap_or(U256::ZERO),
authorization_list,
true,
)
} else if to.is_none() {
let res = self.executor.deploy(
from,
Expand Down Expand Up @@ -282,9 +291,20 @@ impl ScriptRunner {
to: Address,
calldata: Bytes,
value: U256,
authorization_list: Option<Vec<SignedAuthorization>>,
commit: bool,
) -> Result<ScriptResult> {
let mut res = self.executor.call_raw(from, to, calldata.clone(), value)?;
let mut res = if let Some(authorization_list) = authorization_list {
self.executor.call_raw_with_authorization(
from,
to,
calldata.clone(),
value,
authorization_list,
)?
} else {
self.executor.call_raw(from, to, calldata.clone(), value)?
};
let mut gas_used = res.gas_used;

// We should only need to calculate realistic gas costs when preparing to broadcast
Expand Down
5 changes: 3 additions & 2 deletions crates/script/src/simulate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ impl PreSimulationState {
// Executes all transactions from the different forks concurrently.
let futs = transactions
.into_iter()
.map(|transaction| async {
.map(|mut transaction| async {
let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write();
let tx = transaction.tx_mut();

let tx = transaction.tx();
let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None };
let result = runner
.simulate(
Expand All @@ -124,6 +124,7 @@ impl PreSimulationState {
to,
tx.input().map(Bytes::copy_from_slice),
tx.value(),
tx.authorization_list(),
)
.wrap_err("Internal EVM error during simulation")?;

Expand Down
2 changes: 2 additions & 0 deletions testdata/cheats/Vm.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading