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 9 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
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.

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

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

/// Create an EIP-7702 authorization for delegation
#[cheatcode(group = Scripting)]
function createDelegation(address implementation, uint64 nonce) external returns (bytes32);

/// Sign an EIP-7702 authorization for delegation
#[cheatcode(group = Scripting)]
function signDelegation(bytes32 delegation, 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, uint64 nonce, 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
9 changes: 8 additions & 1 deletion 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,9 @@ pub struct Cheatcodes {
/// execution block environment.
pub block: Option<BlockEnv>,

/// EIP-7702 delegations
pub delegations: HashMap<Address, 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 +503,7 @@ impl Cheatcodes {
labels: config.labels.clone(),
config,
block: Default::default(),
delegations: Default::default(),
gas_price: Default::default(),
prank: Default::default(),
expected_revert: Default::default(),
Expand Down
69 changes: 66 additions & 3 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::SignedAuthorization;
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
use std::sync::Arc;

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

impl Cheatcode for createDelegationCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { implementation, nonce } = self;
let auth = Authorization {
address: *implementation,
nonce: *nonce,
chain_id: U256::from(ccx.ecx.env.cfg.chain_id),
};
let hash = auth.signature_hash();
Ok(hash.to_vec())
}
}

impl Cheatcode for attachDelegationCall {
// @todo - change this to apply_full?
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { implementation, nonce, v, r, s } = self;
let auth = Authorization {
address: *implementation,
nonce: *nonce,
chain_id: U256::from(ccx.ecx.env.cfg.chain_id),
};
let addr = auth.address;
let sig = Signature::from_rs_and_parity(
U256::try_from(*r).unwrap(),
U256::try_from(*s).unwrap(),
alloy_primitives::Parity::from(*v == 1),
)?;
let signed_auth = auth.into_signed(sig);
// store in CheatcodeState
ccx.state.delegations.insert(addr, signed_auth);
Ok(Default::default())
}
}

impl Cheatcode for signDelegationCall {
fn apply_stateful(&self, _: &mut CheatsCtxt) -> Result {
klkvr marked this conversation as resolved.
Show resolved Hide resolved
let Self { delegation, privateKey } = self;
let signer = super::crypto::parse_wallet(privateKey)?;
let sig = signer.sign_hash_sync(delegation)?;
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 Expand Up @@ -80,6 +134,8 @@ pub struct Broadcast {
pub depth: u64,
/// Whether the prank stops by itself after the next call
pub single_call: bool,
/// EIP-7702 delegation
pub delegation: Option<SignedAuthorization>,
}

/// Contains context for wallet management.
Expand Down Expand Up @@ -154,18 +210,24 @@ fn broadcast(ccx: &mut CheatsCtxt, new_origin: Option<&Address>, single_call: bo
ensure!(ccx.state.broadcast.is_none(), "a broadcast is active already");

let mut new_origin = new_origin.cloned();
let mut delegation = None;

if new_origin.is_none() {
let mut wallets = ccx.state.wallets().inner.lock();
if let Some(provided_sender) = wallets.provided_sender {
new_origin = Some(provided_sender);
let mut wallets = ccx.state.wallets().inner.lock();
if let Some(provided_sender) = wallets.provided_sender {
new_origin = Some(provided_sender);
delegation = ccx.state.delegations.get(&provided_sender).cloned();
} else {
let signers = wallets.multi_wallet.signers()?;
if signers.len() == 1 {
let address = signers.keys().next().unwrap();
new_origin = Some(*address);
delegation = ccx.state.delegations.get(address).cloned();
}
}
} else if let Some(addr) = new_origin {
// check delegation for explicitly provided address
delegation = ccx.state.delegations.get(&addr).cloned();
}

let broadcast = Broadcast {
Expand All @@ -174,6 +236,7 @@ fn broadcast(ccx: &mut CheatsCtxt, new_origin: Option<&Address>, single_call: bo
original_origin: ccx.ecx.env.tx.caller,
depth: ccx.ecx.journaled_state.depth(),
single_call,
delegation,
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
};
debug!(target: "cheatcodes", ?broadcast, "started");
ccx.state.broadcast = Some(broadcast);
Expand Down
1 change: 1 addition & 0 deletions crates/script/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ foundry-cheatcodes.workspace = true
foundry-wallets.workspace = true
foundry-linking.workspace = true
forge-script-sequence.workspace = true
revm = { workspace = true, default-features = false, features = ["std"] }

serde.workspace = true
eyre.workspace = true
Expand Down
3 changes: 3 additions & 0 deletions testdata/cheats/Vm.sol

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

20 changes: 20 additions & 0 deletions testdata/default/cheats/AttachDelegation.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "cheats/Vm.sol";

contract AttachDelegationTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);
uint256 pk = 77814517325470205911140941194401928579557062014761831930645393041380819009408;
address public ACCOUNT_A = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;

function testCreateDelegation(address implementation, uint64 nonce) public {
bytes32 delegation = vm.createDelegation(implementation, nonce);
(uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(delegation, pk);

vm.attachDelegation(implementation, nonce, v, r, s);
Copy link
Member

Choose a reason for hiding this comment

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

I believe delegations would be attached to separate transactions vs broadcast blocks, so it would be something like

vm.startBroadcast();
vm.attachDelegation(implementation, nonce, v, r, s);
// this transactions would be broadcasted along with attached 7702 delegation
token.transfer(....);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for the comment! looking at this closer - since 7702 makes the EOA itself the target of the tx, i think we need to route all calls through the EOA first.

right now we can do this with a raw call:

vm.startBroadcast();
vm.attachDelegation(implementation, nonce, v, r, s);
alice.call(abi.encodeCall(implementation.execute, (calls)));

but for better UX, maybe we could either:

  1. change attachDelegation to take authority instead of implementation (since we have the implementation in the call itself):
vm.attachDelegation(alice, nonce, v, r, s);
implementation.execute(calls);
  1. or add a helper like:
vm.delegatedCall(alice, implementation.execute, calls);

what do you think would be more ergonomic for users?

vm.broadcast();
// @todo - assert a transaction's msg.sender is implementation address
}
}