Skip to content

Commit

Permalink
Support EIP-7702 Delegations in Forge (foundry-rs#9236)
Browse files Browse the repository at this point in the history
* 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 <klkvrr@gmail.com>
Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com>
  • Loading branch information
3 people authored and rplusq committed Nov 29, 2024
1 parent 93a2bd9 commit 8906af0
Show file tree
Hide file tree
Showing 13 changed files with 477 additions and 18 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

91 changes: 91 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.

1 change: 1 addition & 0 deletions crates/cheatcodes/spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ impl Cheatcodes<'static> {
Vm::Gas::STRUCT.clone(),
Vm::DebugStep::STRUCT.clone(),
Vm::BroadcastTxSummary::STRUCT.clone(),
Vm::SignedDelegation::STRUCT.clone(),
]),
enums: Cow::Owned(vec![
Vm::CallerMode::ENUM.clone(),
Expand Down
28 changes: 28 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ interface Vm {
bool success;
}

/// Holds a signed EIP-7702 authorization for an authority account to delegate to an implementation.
struct SignedDelegation {
/// The y-parity of the recovered secp256k1 signature (0 or 1).
uint8 v;
/// First 32 bytes of the signature.
bytes32 r;
/// Second 32 bytes of the signature.
bytes32 s;
/// The current nonce of the authority account at signing time.
/// Used to ensure signature can't be replayed after account nonce changes.
uint64 nonce;
/// Address of the contract implementation that will be delegated to.
/// Gets encoded into delegation code: 0xef0100 || implementation.
address implementation;
}

// ======== EVM ========

/// Gets the address for a given private key.
Expand Down Expand Up @@ -2018,6 +2034,18 @@ 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 (SignedDelegation memory signedDelegation);

/// Designate the next call as an EIP-7702 transaction
#[cheatcode(group = Scripting)]
function attachDelegation(SignedDelegation memory signedDelegation) external;

/// Sign an EIP-7702 authorization and designate the next call as an EIP-7702 transaction
#[cheatcode(group = Scripting)]
function signAndAttachDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation);

/// 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 @@ -1014,18 +1023,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
89 changes: 89 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::{Signature, 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,92 @@ impl Cheatcode for broadcast_2Call {
}
}

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

let auth = Authorization {
address: *implementation,
nonce: *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),
);
write_delegation(ccx, signed_auth.clone())?;
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 signer = PrivateKeySigner::from_bytes(&B256::from(*privateKey))?;
let authority = signer.address();
let (auth, nonce) = create_auth(ccx, *implementation, authority)?;
let sig = signer.sign_hash_sync(&auth.signature_hash())?;
Ok(sig_to_delegation(sig, nonce, *implementation).abi_encode())
}
}

impl Cheatcode for signAndAttachDelegationCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { implementation, privateKey } = self;
let signer = PrivateKeySigner::from_bytes(&B256::from(*privateKey))?;
let authority = signer.address();
let (auth, nonce) = create_auth(ccx, *implementation, authority)?;
let sig = signer.sign_hash_sync(&auth.signature_hash())?;
let signed_auth = sig_to_auth(sig, auth);
write_delegation(ccx, signed_auth.clone())?;
ccx.state.active_delegation = Some(signed_auth);
Ok(sig_to_delegation(sig, nonce, *implementation).abi_encode())
}
}

fn create_auth(
ccx: &mut CheatsCtxt,
implementation: Address,
authority: Address,
) -> Result<(Authorization, u64)> {
let authority_acc = ccx.ecx.journaled_state.load_account(authority, &mut ccx.ecx.db)?;
let nonce = authority_acc.data.info.nonce;
Ok((
Authorization { address: implementation, nonce, chain_id: ccx.ecx.env.cfg.chain_id },
nonce,
))
}

fn write_delegation(ccx: &mut CheatsCtxt, auth: SignedAuthorization) -> Result<()> {
let authority = auth.recover_authority().map_err(|e| format!("{e}"))?;
let authority_acc = ccx.ecx.journaled_state.load_account(authority, &mut ccx.ecx.db)?;
if authority_acc.data.info.nonce != auth.nonce {
return Err("invalid nonce".into());
}
authority_acc.data.info.nonce += 1;
let bytecode = Bytecode::new_eip7702(*auth.address());
ccx.ecx.journaled_state.set_code(authority, bytecode);
Ok(())
}

fn sig_to_delegation(sig: Signature, nonce: u64, implementation: Address) -> SignedDelegation {
SignedDelegation {
v: sig.v().y_parity() as u8,
r: sig.r().into(),
s: sig.s().into(),
nonce,
implementation,
}
}

fn sig_to_auth(sig: Signature, auth: Authorization) -> SignedAuthorization {
SignedAuthorization::new_unchecked(auth, sig.v().y_parity() as u8, sig.r(), sig.s())
}

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
Loading

0 comments on commit 8906af0

Please sign in to comment.