From 2467a3f0dceffe04cca670af1aa5d26a7430b724 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 30 Oct 2024 15:34:54 +0700 Subject: [PATCH 01/54] add EIP-7702 cheatcodes: createDelegation, signDelegation, attachDelegation --- crates/cheatcodes/spec/src/vm.rs | 12 ++++++++++++ testdata/cheats/Vm.sol | 3 +++ 2 files changed, 15 insertions(+) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 7f27e1aa63b78..b030b4c800460 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1865,6 +1865,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; + // ======== Utilities ======== // -------- Strings -------- diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 335ce83d08964..de5c1b4607e6f 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -145,6 +145,7 @@ interface Vm { function assertTrue(bool condition, string calldata error) external pure; function assume(bool condition) external pure; function assumeNoRevert() external pure; + function attachDelegation(address implementation, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; function blobBaseFee(uint256 newBlobBaseFee) external; function blobhashes(bytes32[] calldata hashes) external; function breakpoint(string calldata char) external; @@ -163,6 +164,7 @@ interface Vm { function cool(address target) external; function copyFile(string calldata from, string calldata to) external returns (uint64 copied); function copyStorage(address from, address to) external; + function createDelegation(address implementation, uint64 nonce) external returns (bytes32); function createDir(string calldata path, bool recursive) external; function createFork(string calldata urlOrAlias) external returns (uint256 forkId); function createFork(string calldata urlOrAlias, uint256 blockNumber) external returns (uint256 forkId); @@ -414,6 +416,7 @@ interface Vm { function signCompact(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(address signer, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); + function signDelegation(bytes32 delegation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); function sign(Wallet calldata wallet, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); From b16edd800f182585b2b6eb62d434179c38fcddaf Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 30 Oct 2024 15:42:47 +0700 Subject: [PATCH 02/54] add cheatcode implementations for EIP-7702: createDelegationCall, signDelegationCall, attachDelegationCall; modify broadcast to check if sender has a delegation --- crates/cheatcodes/src/script.rs | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 93d5aaaf8d349..8d7e2919d267a 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -2,9 +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_sol_types::SolValue; use alloy_signer_local::PrivateKeySigner; use foundry_wallets::{multi_wallet::MultiWallet, WalletSigner}; use parking_lot::Mutex; +use revm::primitives::SignedAuthorization; use std::sync::Arc; impl Cheatcode for broadcast_0Call { @@ -28,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 { + 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 { + 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; @@ -72,6 +127,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, } /// Contains context for wallet management. @@ -131,20 +188,28 @@ 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() { if let Some(script_wallets) = ccx.state.script_wallets() { let mut script_wallets = script_wallets.inner.lock(); if let Some(provided_sender) = script_wallets.provided_sender { new_origin = Some(provided_sender); + // check if this sender has a delegation + delegation = ccx.state.delegations.get(&provided_sender).cloned(); } else { let signers = script_wallets.multi_wallet.signers()?; if signers.len() == 1 { let address = signers.keys().next().unwrap(); new_origin = Some(*address); + // check delegation for single signer case + 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 { @@ -153,6 +218,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 }; debug!(target: "cheatcodes", ?broadcast, "started"); ccx.state.broadcast = Some(broadcast); From a286c7d69275678b63ea80378928e67609ec5db9 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 30 Oct 2024 15:43:32 +0700 Subject: [PATCH 03/54] add delegations hashmap to Cheatcodes struct --- crates/cheatcodes/src/inspector.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index fe8834d5f5a82..c87871374908d 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -46,7 +46,7 @@ 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 rustc_hash::FxHashMap; @@ -366,6 +366,9 @@ pub struct Cheatcodes { /// execution block environment. pub block: Option, + /// EIP-7702 delegations + pub delegations: HashMap, + /// The gas price. /// /// Used in the cheatcode handler to overwrite the gas price separately from the gas price @@ -485,6 +488,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(), From a6de137eb7eef3cf8c8915992eb1e3d0bbb17f7e Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 30 Oct 2024 15:48:42 +0700 Subject: [PATCH 04/54] add revm crate --- Cargo.lock | 1 + crates/script/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 70ea8bef4cab0..c294e75656c0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3485,6 +3485,7 @@ dependencies = [ "indicatif", "itertools 0.13.0", "parking_lot", + "revm", "revm-inspectors", "semver 1.0.23", "serde", diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 9c042661775ce..67580373d817c 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -23,6 +23,7 @@ foundry-debugger.workspace = true foundry-cheatcodes.workspace = true foundry-wallets.workspace = true foundry-linking.workspace = true +revm = { workspace = true, default-features = false, features = ["std"] } serde.workspace = true eyre.workspace = true From 619aa1e969a5f157cb44686ea57c0b1c25038ff1 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 30 Oct 2024 15:49:49 +0700 Subject: [PATCH 05/54] create AttachDelegationTest for EIP-7702 transactions --- .../default/cheats/AttachDelegation.t.sol | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 testdata/default/cheats/AttachDelegation.t.sol diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol new file mode 100644 index 0000000000000..164868935931c --- /dev/null +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -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.broadcast(); + vm.attachDelegation(implementation, nonce, v, r, s); + // @todo - assert a transaction's msg.sender is implementation address + } +} From d5ae337b4bd92d928f9de00b6b545a1fed18c345 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 30 Oct 2024 15:52:50 +0700 Subject: [PATCH 06/54] regen cheatcodes.json --- crates/cheatcodes/assets/cheatcodes.json | 60 ++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 9b6c67ddd195f..a3f9e17a05f60 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -2991,6 +2991,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "attachDelegation", + "description": "Designate the next call as an EIP-7702 transaction", + "declaration": "function attachDelegation(address implementation, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external;", + "visibility": "external", + "mutability": "", + "signature": "attachDelegation(address,uint64,uint8,bytes32,bytes32)", + "selector": "0x2d8e2226", + "selectorBytes": [ + 45, + 142, + 34, + 38 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "blobBaseFee", @@ -3351,6 +3371,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "createDelegation", + "description": "Create an EIP-7702 authorization for delegation", + "declaration": "function createDelegation(address implementation, uint64 nonce) external returns (bytes32);", + "visibility": "external", + "mutability": "", + "signature": "createDelegation(address,uint64)", + "selector": "0x0377eeb5", + "selectorBytes": [ + 3, + 119, + 238, + 181 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "createDir", @@ -8381,6 +8421,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "signDelegation", + "description": "Sign an EIP-7702 authorization for delegation", + "declaration": "function signDelegation(bytes32 delegation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);", + "visibility": "external", + "mutability": "", + "signature": "signDelegation(bytes32,uint256)", + "selector": "0x75501e61", + "selectorBytes": [ + 117, + 80, + 30, + 97 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "signP256", From 05cdad8a8b6d6f4254061ec93757ad557fed867e Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 31 Oct 2024 09:49:44 +0700 Subject: [PATCH 07/54] cargo fmt --- crates/cheatcodes/spec/src/vm.rs | 2 +- crates/cheatcodes/src/inspector.rs | 5 ++++- crates/cheatcodes/src/script.rs | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index b030b4c800460..d4d8b1a2a8ef6 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1868,7 +1868,7 @@ interface Vm { /// 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); diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index c87871374908d..d5a71722630af 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -46,7 +46,10 @@ use revm::{ EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, }, - primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId, EOF_MAGIC_BYTES}, + primitives::{ + BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId, + EOF_MAGIC_BYTES, + }, EvmContext, InnerEvmContext, Inspector, }; use rustc_hash::FxHashMap; diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 8d7e2919d267a..ef71e0f17f075 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -4,8 +4,8 @@ use crate::{Cheatcode, CheatsCtxt, Result, Vm::*}; use alloy_primitives::{Address, B256, U256}; use alloy_rpc_types::Authorization; use alloy_signer::{Signature, SignerSync}; -use alloy_sol_types::SolValue; 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; @@ -58,7 +58,7 @@ impl Cheatcode for attachDelegationCall { let sig = Signature::from_rs_and_parity( U256::try_from(*r).unwrap(), U256::try_from(*s).unwrap(), - alloy_primitives::Parity::from(*v == 1) + alloy_primitives::Parity::from(*v == 1), )?; let signed_auth = auth.into_signed(sig); // store in CheatcodeState @@ -218,7 +218,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 + delegation, }; debug!(target: "cheatcodes", ?broadcast, "started"); ccx.state.broadcast = Some(broadcast); From 1d8f42e0a53bd3be7bff7e900993567926af0cbe Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 31 Oct 2024 10:04:02 +0700 Subject: [PATCH 08/54] move broadcast under attachDelegation --- testdata/default/cheats/AttachDelegation.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index 164868935931c..0811171519671 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -13,8 +13,8 @@ contract AttachDelegationTest is DSTest { bytes32 delegation = vm.createDelegation(implementation, nonce); (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(delegation, pk); - vm.broadcast(); vm.attachDelegation(implementation, nonce, v, r, s); + vm.broadcast(); // @todo - assert a transaction's msg.sender is implementation address } } From da62cc8e3e5cfe442b3edd810b286e8484365541 Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:18:42 +0700 Subject: [PATCH 09/54] 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 --- crates/cheatcodes/src/script.rs | 39 ++++++++++----------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 7b7a92080eb5a..20c0f0f6848d1 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -8,7 +8,6 @@ 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; use std::sync::Arc; impl Cheatcode for broadcast_0Call { @@ -32,19 +31,6 @@ 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 { @@ -52,7 +38,7 @@ impl Cheatcode for attachDelegationCall { let auth = Authorization { address: *implementation, nonce: *nonce, - chain_id: U256::from(ccx.ecx.env.cfg.chain_id), + chain_id: ccx.ecx.env.cfg.chain_id, }; let addr = auth.address; let sig = Signature::from_rs_and_parity( @@ -61,17 +47,23 @@ impl Cheatcode for attachDelegationCall { alloy_primitives::Parity::from(*v == 1), )?; let signed_auth = auth.into_signed(sig); - // store in CheatcodeState ccx.state.delegations.insert(addr, signed_auth); + ccx.state.active_delegation = Some(addr); Ok(Default::default()) } } impl Cheatcode for signDelegationCall { - fn apply_stateful(&self, _: &mut CheatsCtxt) -> Result { - let Self { delegation, privateKey } = self; + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { implementation, nonce, privateKey } = self; + let auth = Authorization { + address: *implementation, + nonce: *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(delegation)?; + let sig = signer.sign_hash_sync(&hash)?; Ok(encode_delegation_sig(sig)) } } @@ -134,8 +126,6 @@ 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, } /// Contains context for wallet management. @@ -210,24 +200,18 @@ 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); - 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 { @@ -236,7 +220,6 @@ 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, }; debug!(target: "cheatcodes", ?broadcast, "started"); ccx.state.broadcast = Some(broadcast); From bad6230ee4577e1a6b21e2da335f10f4cb16d76e Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:18:59 +0700 Subject: [PATCH 10/54] remove revm import from workspace --- crates/script/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 1d1d740919200..0d4810da66c14 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -24,7 +24,6 @@ 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 From b3666aacdf15929b9c49fc1e1c9e03e029685c0e Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:20:16 +0700 Subject: [PATCH 11/54] combine createDelegation logic inton signDelegation for simplicity --- crates/cheatcodes/spec/src/vm.rs | 6 +----- testdata/cheats/Vm.sol | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index c2684ec4acf92..cb928d14b6fb1 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1973,13 +1973,9 @@ 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); + function signDelegation(address implementation, uint64 nonce, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); /// Designate the next call as an EIP-7702 transaction #[cheatcode(group = Scripting)] diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index e098843bbf7d3..b7a32a7874397 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -169,7 +169,6 @@ interface Vm { function cool(address target) external; function copyFile(string calldata from, string calldata to) external returns (uint64 copied); function copyStorage(address from, address to) external; - function createDelegation(address implementation, uint64 nonce) external returns (bytes32); function createDir(string calldata path, bool recursive) external; function createFork(string calldata urlOrAlias) external returns (uint256 forkId); function createFork(string calldata urlOrAlias, uint256 blockNumber) external returns (uint256 forkId); @@ -434,7 +433,7 @@ interface Vm { function signCompact(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(address signer, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); - function signDelegation(bytes32 delegation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); + function signDelegation(address implementation, uint64 nonce, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); function sign(Wallet calldata wallet, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); From 376a092db75c7d7002749aa5385a4d9d9f14cc5a Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:22:24 +0700 Subject: [PATCH 12/54] remove revm from forge script deps --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 8e68d69727c71..de6f11ea9b219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3490,7 +3490,6 @@ dependencies = [ "indicatif", "itertools 0.13.0", "parking_lot", - "revm", "revm-inspectors", "semver 1.0.23", "serde", From 93963ee1c0b66467c5d59245dc62527f0907c885 Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:22:38 +0700 Subject: [PATCH 13/54] combine createDelegation with signDelegation --- crates/cheatcodes/assets/cheatcodes.json | 34 +++++------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 9b78aa820eca8..cafb8fb00ef77 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -3496,26 +3496,6 @@ "status": "stable", "safety": "safe" }, - { - "func": { - "id": "createDelegation", - "description": "Create an EIP-7702 authorization for delegation", - "declaration": "function createDelegation(address implementation, uint64 nonce) external returns (bytes32);", - "visibility": "external", - "mutability": "", - "signature": "createDelegation(address,uint64)", - "selector": "0x0377eeb5", - "selectorBytes": [ - 3, - 119, - 238, - 181 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, { "func": { "id": "createDir", @@ -8810,16 +8790,16 @@ "func": { "id": "signDelegation", "description": "Sign an EIP-7702 authorization for delegation", - "declaration": "function signDelegation(bytes32 delegation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);", + "declaration": "function signDelegation(address implementation, uint64 nonce, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);", "visibility": "external", "mutability": "", - "signature": "signDelegation(bytes32,uint256)", - "selector": "0x75501e61", + "signature": "signDelegation(address,uint64,uint256)", + "selector": "0xc6d4116d", "selectorBytes": [ - 117, - 80, - 30, - 97 + 198, + 212, + 17, + 109 ] }, "group": "scripting", From df33332460bacccdec521e54326bb553b818fe9b Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:37:36 +0700 Subject: [PATCH 14/54] WIP - refactor test to use SimpleDelegateContract and ERC20 - test currently failing bc 7702 implementation.execute not executed as Alice EOA --- .../default/cheats/AttachDelegation.t.sol | 84 +++++++++++++++++-- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index 0811171519671..be362a76f2c94 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -5,16 +5,84 @@ import "ds-test/test.sol"; import "cheats/Vm.sol"; contract AttachDelegationTest is DSTest { + event Executed(address indexed to, uint256 value, bytes data); Vm constant vm = Vm(HEVM_ADDRESS); - uint256 pk = 77814517325470205911140941194401928579557062014761831930645393041380819009408; - address public ACCOUNT_A = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + uint256 alice_pk = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + address alice = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + uint256 bob_pk=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; + address bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; - function testCreateDelegation(address implementation, uint64 nonce) public { - bytes32 delegation = vm.createDelegation(implementation, nonce); - (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(delegation, pk); + function testAttachDelegation(uint64 nonce) public { + SimpleDelegateContract implementation = new SimpleDelegateContract(); + ERC20 token = new ERC20(alice); - vm.attachDelegation(implementation, nonce, v, r, s); - vm.broadcast(); - // @todo - assert a transaction's msg.sender is implementation address + (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), nonce, alice_pk); + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); + bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); + calls[0] = SimpleDelegateContract.Call({ + to: address(token), + data: data, + value: 0 + }); + // executing as bob to make clear that we don't need to execute the tx as alice + vm.broadcast(bob_pk); + vm.attachDelegation(address(implementation), nonce, v, r, s); + // TODO - how do we specify that the target of this tx is alice's EOA + // fails if msg.sender is not implementation address (minter) + implementation.execute(calls); + + assertEq(token.balanceOf(bob), 100); + } +} + +contract SimpleDelegateContract { + event WhoAmI(address); + event Executed(address indexed to, uint256 value, bytes data); + struct Call { + bytes data; + address to; + uint256 value; } + + function execute(Call[] memory calls) external payable { + emit WhoAmI(msg.sender); + for (uint256 i = 0; i < calls.length; i++) { + emit WhoAmI(address(this)); + Call memory call = calls[i]; + (bool success, bytes memory result) = call.to.call{value: call.value}(call.data); + require(success, string(result)); + emit Executed(call.to, call.value, call.data); + } + } + + receive() external payable {} } + +contract ERC20 { + event WhoAmI(address); + address public minter; + mapping(address => uint256) private _balances; + + constructor(address _minter) { + minter = _minter; + emit WhoAmI(minter); + } + + function mint(uint256 amount, address to) public { + _mint(to, amount); + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function _mint(address account, uint256 amount) internal { + emit WhoAmI(msg.sender); + emit WhoAmI(tx.origin); + require(msg.sender == minter, "ERC20: msg.sender is not minter"); + require(account != address(0), "ERC20: mint to the zero address"); + unchecked { + _balances[account] += amount; + } + } +} \ No newline at end of file From 912ce6d442558c5f96c40798839ebcf600b5cbdb Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 3 Nov 2024 14:54:18 +0700 Subject: [PATCH 15/54] add logic to include authorization_list for EIP 7702 in TransactionRequest by searching delegations hash map by active_delegation --- crates/cheatcodes/src/inspector.rs | 41 ++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 2260ae492adcf..9789f002e90de 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -379,6 +379,8 @@ pub struct Cheatcodes { /// EIP-7702 delegations pub delegations: HashMap, + pub active_delegation: Option
, + /// The gas price. /// /// Used in the cheatcode handler to overwrite the gas price separately from the gas price @@ -504,6 +506,7 @@ impl Cheatcodes { config, block: Default::default(), delegations: Default::default(), + active_delegation: Default::default(), gas_price: Default::default(), prank: Default::default(), expected_revert: Default::default(), @@ -1008,21 +1011,37 @@ where { let account = ecx.journaled_state.state().get_mut(&broadcast.new_origin).unwrap(); + let 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), + gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, + authorization_list: self.active_delegation + .and_then(|addr| self.delegations.remove(&addr)) + .map(|auth| vec![auth]), + ..Default::default() + }; + + // TODO - fails bc to build 7702 tx max_fee_per_gas, max_priority_fee_per_gas, gas are None + let tx_req = match tx_req.build_typed_tx() { + Ok(typed) => { + debug!(target: "cheatcodes", ?typed, "built typed tx"); + typed.into() + } + Err(tx) => { + debug!(target: "cheatcodes", "failed to build typed tx"); + tx + } + }; + 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"); - + // Explicitly increment nonce if calls are not isolated. if !self.config.evm_opts.isolate { let prev = account.info.nonce; From e97e185527a2c197c7af94d1c351a6a16a07f6ff Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 17:19:27 +0700 Subject: [PATCH 16/54] add address authority param to attachDelegation; remove nonce param from signDelegation, as it can be constructed in cheatcode. --- crates/cheatcodes/assets/cheatcodes.json | 28 ++++++++++++------------ crates/cheatcodes/spec/src/vm.rs | 4 ++-- testdata/cheats/Vm.sol | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index cafb8fb00ef77..eb17241a43ae8 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -3080,16 +3080,16 @@ "func": { "id": "attachDelegation", "description": "Designate the next call as an EIP-7702 transaction", - "declaration": "function attachDelegation(address implementation, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external;", + "declaration": "function attachDelegation(address implementation, address authority, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external;", "visibility": "external", "mutability": "", - "signature": "attachDelegation(address,uint64,uint8,bytes32,bytes32)", - "selector": "0x2d8e2226", + "signature": "attachDelegation(address,address,uint64,uint8,bytes32,bytes32)", + "selector": "0xbf9aeef2", "selectorBytes": [ - 45, - 142, - 34, - 38 + 191, + 154, + 238, + 242 ] }, "group": "scripting", @@ -8790,16 +8790,16 @@ "func": { "id": "signDelegation", "description": "Sign an EIP-7702 authorization for delegation", - "declaration": "function signDelegation(address implementation, uint64 nonce, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);", + "declaration": "function signDelegation(address implementation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);", "visibility": "external", "mutability": "", - "signature": "signDelegation(address,uint64,uint256)", - "selector": "0xc6d4116d", + "signature": "signDelegation(address,uint256)", + "selector": "0x5b593c7b", "selectorBytes": [ - 198, - 212, - 17, - 109 + 91, + 89, + 60, + 123 ] }, "group": "scripting", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index cb928d14b6fb1..40540c8b5c908 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1975,11 +1975,11 @@ interface Vm { /// Sign an EIP-7702 authorization for delegation #[cheatcode(group = Scripting)] - function signDelegation(address implementation, uint64 nonce, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); + 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, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; + function attachDelegation(address implementation, address authority, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; /// Returns addresses of available unlocked wallets in the script environment. #[cheatcode(group = Scripting)] diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index b7a32a7874397..d4bf15474c22b 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -148,7 +148,7 @@ interface Vm { function assertTrue(bool condition, string calldata error) external pure; function assume(bool condition) external pure; function assumeNoRevert() external pure; - function attachDelegation(address implementation, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; + function attachDelegation(address implementation, address authority, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; function blobBaseFee(uint256 newBlobBaseFee) external; function blobhashes(bytes32[] calldata hashes) external; function breakpoint(string calldata char) external pure; @@ -433,7 +433,7 @@ interface Vm { function signCompact(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(address signer, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); - function signDelegation(address implementation, uint64 nonce, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); + function signDelegation(address implementation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); function sign(Wallet calldata wallet, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); From 40fd5f107ab6ea368ba90afc6b375043d4b9bce6 Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 17:20:20 +0700 Subject: [PATCH 17/54] remove 7702 tx request construction logic - now handled in attachDelegation cheatcode implementation --- crates/cheatcodes/src/inspector.rs | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 9789f002e90de..3273901620347 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -47,7 +47,7 @@ use revm::{ InterpreterResult, }, primitives::{ - BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId, + BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, EOF_MAGIC_BYTES, }, EvmContext, InnerEvmContext, Inspector, @@ -376,11 +376,6 @@ pub struct Cheatcodes { /// execution block environment. pub block: Option, - /// EIP-7702 delegations - pub delegations: HashMap, - - pub active_delegation: Option
, - /// The gas price. /// /// Used in the cheatcode handler to overwrite the gas price separately from the gas price @@ -505,8 +500,6 @@ impl Cheatcodes { labels: config.labels.clone(), config, block: Default::default(), - delegations: Default::default(), - active_delegation: Default::default(), gas_price: Default::default(), prank: Default::default(), expected_revert: Default::default(), @@ -1018,30 +1011,16 @@ where { input: TransactionInput::new(call.input.clone()), nonce: Some(account.info.nonce), gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, - authorization_list: self.active_delegation - .and_then(|addr| self.delegations.remove(&addr)) - .map(|auth| vec![auth]), ..Default::default() }; - // TODO - fails bc to build 7702 tx max_fee_per_gas, max_priority_fee_per_gas, gas are None - let tx_req = match tx_req.build_typed_tx() { - Ok(typed) => { - debug!(target: "cheatcodes", ?typed, "built typed tx"); - typed.into() - } - Err(tx) => { - debug!(target: "cheatcodes", "failed to build typed tx"); - tx - } - }; - + self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: ecx.db.active_fork_url(), transaction: tx_req.into(), }); debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call"); - + // Explicitly increment nonce if calls are not isolated. if !self.config.evm_opts.isolate { let prev = account.info.nonce; From bdef83c89542677effeb8143820b19bb91bffade Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 17:22:55 +0700 Subject: [PATCH 18/54] refactor attachDelegation cheatcode implementation to handle verifying signature and setting bytecode on EOA; refactor signDelegation cheatcode implementation to get nonce from signer --- crates/cheatcodes/src/script.rs | 54 +++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 20c0f0f6848d1..92e8a15a454dd 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -3,11 +3,12 @@ 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::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 { @@ -32,33 +33,60 @@ impl Cheatcode for broadcast_2Call { } 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 Self { implementation, authority, nonce, v, r, s } = self; + let auth = Authorization { address: *implementation, nonce: *nonce, chain_id: ccx.ecx.env.cfg.chain_id, }; - let addr = auth.address; - let sig = Signature::from_rs_and_parity( + let signed_auth = SignedAuthorization::new_unchecked( + auth, + *v as u8, U256::try_from(*r).unwrap(), - U256::try_from(*s).unwrap(), - alloy_primitives::Parity::from(*v == 1), - )?; - let signed_auth = auth.into_signed(sig); - ccx.state.delegations.insert(addr, signed_auth); - ccx.state.active_delegation = Some(addr); + U256::try_from(*s).unwrap() + ); + + // 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()); + } + + // verify nonce matches + let mut authority_acc = ccx.ecx.journaled_state + .load_account(*authority, &mut ccx.ecx.db)?; + if authority_acc.info.nonce != *nonce { + return Err("nonce mismatch".into()); + } + + // write delegation code + let bytecode = Bytecode::new_eip7702(*implementation); + authority_acc.info.code = Some(bytecode.clone()); + authority_acc.info.code_hash = bytecode.hash_slow(); + authority_acc.mark_touch(); + Ok(Default::default()) } } impl Cheatcode for signDelegationCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { - let Self { implementation, nonce, privateKey } = self; + let Self { implementation, privateKey } = self; + let key_bytes = B256::from(*privateKey); + let signer: alloy_signer_local::LocalSigner> = 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: *nonce, + nonce, chain_id: ccx.ecx.env.cfg.chain_id, }; let hash = auth.signature_hash(); From e4082b1a6ca233e3f44ccbd472a6486e75a74edd Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 18:01:02 +0700 Subject: [PATCH 19/54] remove nonce param from attachDelegation cheatcode in favor of loading from authority account --- crates/cheatcodes/assets/cheatcodes.json | 14 +++++++------- crates/cheatcodes/spec/src/vm.rs | 2 +- crates/cheatcodes/src/script.rs | 12 +++++++++--- testdata/cheats/Vm.sol | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index eb17241a43ae8..2abf9e80fcd27 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -3080,16 +3080,16 @@ "func": { "id": "attachDelegation", "description": "Designate the next call as an EIP-7702 transaction", - "declaration": "function attachDelegation(address implementation, address authority, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external;", + "declaration": "function attachDelegation(address implementation, address authority, uint8 v, bytes32 r, bytes32 s) external;", "visibility": "external", "mutability": "", - "signature": "attachDelegation(address,address,uint64,uint8,bytes32,bytes32)", - "selector": "0xbf9aeef2", + "signature": "attachDelegation(address,address,uint8,bytes32,bytes32)", + "selector": "0x9c509458", "selectorBytes": [ - 191, - 154, - 238, - 242 + 156, + 80, + 148, + 88 ] }, "group": "scripting", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 40540c8b5c908..9c996471bdd6c 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1979,7 +1979,7 @@ interface Vm { /// Designate the next call as an EIP-7702 transaction #[cheatcode(group = Scripting)] - function attachDelegation(address implementation, address authority, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; + 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)] diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 92e8a15a454dd..ad868108b607f 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -34,11 +34,17 @@ impl Cheatcode for broadcast_2Call { impl Cheatcode for attachDelegationCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { - let Self { implementation, authority, nonce, v, r, s } = self; + let Self { implementation, authority, v, r, s } = self; + + let nonce = ccx.ecx.journaled_state + .load_account(*authority, &mut ccx.ecx.db)? + .data + .info + .nonce; let auth = Authorization { address: *implementation, - nonce: *nonce, + nonce, chain_id: ccx.ecx.env.cfg.chain_id, }; let signed_auth = SignedAuthorization::new_unchecked( @@ -58,7 +64,7 @@ impl Cheatcode for attachDelegationCall { // verify nonce matches let mut authority_acc = ccx.ecx.journaled_state .load_account(*authority, &mut ccx.ecx.db)?; - if authority_acc.info.nonce != *nonce { + if authority_acc.info.nonce != nonce { return Err("nonce mismatch".into()); } diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index d4bf15474c22b..05c89c434dd49 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -148,7 +148,7 @@ interface Vm { function assertTrue(bool condition, string calldata error) external pure; function assume(bool condition) external pure; function assumeNoRevert() external pure; - function attachDelegation(address implementation, address authority, uint64 nonce, uint8 v, bytes32 r, bytes32 s) external; + function attachDelegation(address implementation, address authority, uint8 v, bytes32 r, bytes32 s) external; function blobBaseFee(uint256 newBlobBaseFee) external; function blobhashes(bytes32[] calldata hashes) external; function breakpoint(string calldata char) external pure; From f18a0e8974fc4abe26f4db891846f2de5f2f22f5 Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 18:01:49 +0700 Subject: [PATCH 20/54] refactor test to check for code on alice account and call execute on alice account through SimpleDelegateContract --- testdata/default/cheats/AttachDelegation.t.sol | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index be362a76f2c94..82e14d0bdefe8 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -8,15 +8,15 @@ contract AttachDelegationTest is DSTest { event Executed(address indexed to, uint256 value, bytes data); Vm constant vm = Vm(HEVM_ADDRESS); uint256 alice_pk = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; - address alice = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address payable alice = payable(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); uint256 bob_pk=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; address bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; - function testAttachDelegation(uint64 nonce) public { + function testAttachDelegation() public { SimpleDelegateContract implementation = new SimpleDelegateContract(); ERC20 token = new ERC20(alice); - (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), nonce, alice_pk); + (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); calls[0] = SimpleDelegateContract.Call({ @@ -26,10 +26,11 @@ contract AttachDelegationTest is DSTest { }); // executing as bob to make clear that we don't need to execute the tx as alice vm.broadcast(bob_pk); - vm.attachDelegation(address(implementation), nonce, v, r, s); - // TODO - how do we specify that the target of this tx is alice's EOA - // fails if msg.sender is not implementation address (minter) - implementation.execute(calls); + vm.attachDelegation(address(implementation), alice, v, r, s); + + bytes memory code = address(alice).code; + require(code.length > 0, "no code written to alice"); + SimpleDelegateContract(alice).execute(calls); assertEq(token.balanceOf(bob), 100); } From d8ea3ecca9f1ca20832cab9646fd64f6923336ea Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 19:07:01 +0700 Subject: [PATCH 21/54] revert refactor on TransactionRequest --- crates/cheatcodes/src/inspector.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 3273901620347..684f5eb110d2f 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -46,10 +46,7 @@ use revm::{ EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, }, - primitives::{ - BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, - EOF_MAGIC_BYTES, - }, + primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, EOF_MAGIC_BYTES}, EvmContext, InnerEvmContext, Inspector, }; use serde_json::Value; @@ -1004,20 +1001,18 @@ where { let account = ecx.journaled_state.state().get_mut(&broadcast.new_origin).unwrap(); - let 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), - gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, - ..Default::default() - }; - - self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: ecx.db.active_fork_url(), - transaction: tx_req.into(), + 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(), }); debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call"); From 0bd6f422e3924834b23a691c768ac0c8a110391f Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 4 Nov 2024 19:08:46 +0700 Subject: [PATCH 22/54] format --- crates/cheatcodes/src/script.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index ad868108b607f..565c8e0ff8084 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -236,9 +236,9 @@ fn broadcast(ccx: &mut CheatsCtxt, new_origin: Option<&Address>, single_call: bo let mut new_origin = new_origin.cloned(); 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); } else { let signers = wallets.multi_wallet.signers()?; if signers.len() == 1 { From 8f480fe12d7ecfcdd7f5d13de6ddbd930ded11a3 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 5 Nov 2024 10:05:12 +0700 Subject: [PATCH 23/54] cargo fmt --- crates/cheatcodes/src/script.rs | 48 +++++++++++++-------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 565c8e0ff8084..fb81e00377a23 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -36,44 +36,37 @@ impl Cheatcode for attachDelegationCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { implementation, authority, v, r, s } = self; - 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 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 signed_auth = SignedAuthorization::new_unchecked( auth, *v as u8, U256::try_from(*r).unwrap(), - U256::try_from(*s).unwrap() + U256::try_from(*s).unwrap(), ); // verify signature is from claimed authority - let recovered = signed_auth.recover_authority() - .map_err(|e| format!("{e}"))?; + let recovered = signed_auth.recover_authority().map_err(|e| format!("{e}"))?; if recovered != *authority { return Err("invalid signature".into()); } // verify nonce matches - let mut authority_acc = ccx.ecx.journaled_state - .load_account(*authority, &mut ccx.ecx.db)?; + let mut authority_acc = + ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?; if authority_acc.info.nonce != nonce { return Err("nonce mismatch".into()); } // write delegation code - let bytecode = Bytecode::new_eip7702(*implementation); + let bytecode = Bytecode::new_eip7702(*implementation); authority_acc.info.code = Some(bytecode.clone()); authority_acc.info.code_hash = bytecode.hash_slow(); authority_acc.mark_touch(); - + Ok(Default::default()) } } @@ -82,19 +75,14 @@ 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> = PrivateKeySigner::from_bytes(&key_bytes)?; + let signer: alloy_signer_local::LocalSigner> = + 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 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)?; From 25aeea788e4202372b61d248276e943971098a03 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 5 Nov 2024 10:21:32 +0700 Subject: [PATCH 24/54] fix clippy errors --- crates/cheatcodes/src/script.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index fb81e00377a23..eddf119f61272 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -43,9 +43,9 @@ impl Cheatcode for attachDelegationCall { Authorization { address: *implementation, nonce, chain_id: ccx.ecx.env.cfg.chain_id }; let signed_auth = SignedAuthorization::new_unchecked( auth, - *v as u8, - U256::try_from(*r).unwrap(), - U256::try_from(*s).unwrap(), + *v, + U256::from_be_bytes(r.0), + U256::from_be_bytes(s.0), ); // verify signature is from claimed authority From 4310c2ddda8e8aee5b69c62962e821ec35f95acb Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 6 Nov 2024 12:26:23 +0700 Subject: [PATCH 25/54] remove faulty logic comparing nonce to itself - nonce still checked by recovered signature --- crates/cheatcodes/src/script.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index eddf119f61272..2769a03060ad6 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -36,11 +36,11 @@ impl Cheatcode for attachDelegationCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { implementation, authority, v, r, s } = self; - let nonce = - ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?.data.info.nonce; + let mut authority_acc = + ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?; let auth = - Authorization { address: *implementation, nonce, chain_id: ccx.ecx.env.cfg.chain_id }; + 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, @@ -54,13 +54,6 @@ impl Cheatcode for attachDelegationCall { return Err("invalid signature".into()); } - // verify nonce matches - let mut authority_acc = - ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?; - if authority_acc.info.nonce != nonce { - return Err("nonce mismatch".into()); - } - // write delegation code let bytecode = Bytecode::new_eip7702(*implementation); authority_acc.info.code = Some(bytecode.clone()); From bdc0b78ce5cc132b28b6f04c0a321ceb5965a402 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 6 Nov 2024 12:27:17 +0700 Subject: [PATCH 26/54] add more tests to cover revert cases on attachDelegation and multiple calls via delegation contract --- .../default/cheats/AttachDelegation.t.sol | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index 82e14d0bdefe8..6fd3f6416a415 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -5,17 +5,21 @@ import "ds-test/test.sol"; import "cheats/Vm.sol"; contract AttachDelegationTest is DSTest { - event Executed(address indexed to, uint256 value, bytes data); Vm constant vm = Vm(HEVM_ADDRESS); uint256 alice_pk = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; address payable alice = payable(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); uint256 bob_pk=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; address bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; - function testAttachDelegation() public { - SimpleDelegateContract implementation = new SimpleDelegateContract(); - ERC20 token = new ERC20(alice); + SimpleDelegateContract implementation; + ERC20 token; + + function setUp() public { + implementation = new SimpleDelegateContract(); + token = new ERC20(alice); + } + function testCallSingleDelegation() public { (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); @@ -34,10 +38,52 @@ contract AttachDelegationTest is DSTest { assertEq(token.balanceOf(bob), 100); } + + function testMultiCallDelegation() public { + (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + vm.broadcast(bob_pk); + vm.attachDelegation(address(implementation), alice, v, r, s); + + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](2); + calls[0] = SimpleDelegateContract.Call({ + to: address(token), + data: abi.encodeCall(ERC20.mint, (50, bob)), + value: 0 + }); + calls[1] = SimpleDelegateContract.Call({ + to: address(token), + data: abi.encodeCall(ERC20.mint, (50, address(this))), + value: 0 + }); + + SimpleDelegateContract(alice).execute(calls); + + assertEq(token.balanceOf(bob), 50); + assertEq(token.balanceOf(address(this)), 50); + } + + function testAttachDelegationRevertInvalidSignature() public { + (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + // change v from 1 to 0 + v = 0; + + vm.expectRevert("vm.attachDelegation: invalid signature"); + vm.attachDelegation(address(implementation), alice, v, r, s); + } + + function testDelegationRevertsAfterNonceChange() public { + (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + + vm.broadcast(alice_pk); + // send tx to increment alice's nonce + token.mint(1, bob); + + vm.expectRevert("vm.attachDelegation: invalid signature"); + vm.attachDelegation(address(implementation), alice, v, r, s); + } } contract SimpleDelegateContract { - event WhoAmI(address); event Executed(address indexed to, uint256 value, bytes data); struct Call { bytes data; @@ -46,9 +92,7 @@ contract SimpleDelegateContract { } function execute(Call[] memory calls) external payable { - emit WhoAmI(msg.sender); for (uint256 i = 0; i < calls.length; i++) { - emit WhoAmI(address(this)); Call memory call = calls[i]; (bool success, bytes memory result) = call.to.call{value: call.value}(call.data); require(success, string(result)); @@ -60,13 +104,11 @@ contract SimpleDelegateContract { } contract ERC20 { - event WhoAmI(address); address public minter; mapping(address => uint256) private _balances; constructor(address _minter) { minter = _minter; - emit WhoAmI(minter); } function mint(uint256 amount, address to) public { @@ -78,8 +120,6 @@ contract ERC20 { } function _mint(address account, uint256 amount) internal { - emit WhoAmI(msg.sender); - emit WhoAmI(tx.origin); require(msg.sender == minter, "ERC20: msg.sender is not minter"); require(account != address(0), "ERC20: mint to the zero address"); unchecked { From c10be3e0203c051f05f440c4c2222e99eb55d682 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 6 Nov 2024 12:33:01 +0700 Subject: [PATCH 27/54] cargo fmt --- crates/cheatcodes/src/script.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 2769a03060ad6..e3fbeb3a4ebc7 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -39,8 +39,11 @@ impl Cheatcode for attachDelegationCall { let mut 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 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, From 709099935dc9a31b54c38ac53ea5d37aefc8e6dd Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 7 Nov 2024 17:29:29 +0700 Subject: [PATCH 28/54] 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 --- crates/cheatcodes/src/inspector.rs | 43 ++++++++++++++++++++++-------- crates/cheatcodes/src/script.rs | 4 ++- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 684f5eb110d2f..a59cdbeef6414 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -46,7 +46,7 @@ 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; @@ -373,6 +373,9 @@ pub struct Cheatcodes { /// execution block environment. pub block: Option, + pub delegations: HashMap, + pub active_delegation: Option
, + /// The gas price. /// /// Used in the cheatcode handler to overwrite the gas price separately from the gas price @@ -497,6 +500,8 @@ impl Cheatcodes { labels: config.labels.clone(), config, block: Default::default(), + delegations: Default::default(), + active_delegation: Default::default(), gas_price: Default::default(), prank: Default::default(), expected_revert: Default::default(), @@ -1001,18 +1006,34 @@ 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.and_then(|addr| self.delegations.remove(&addr)) { + tx_req.authorization_list = Some(vec![auth_list]); + tx_req.transaction_type = Some(4); // EIP-7702 + // known working values for 7702 on odyssey + // TODO(7702): These gas values are temporary defaults that work for testing. + // We should properly handle gas estimation for EIP-7702 transactions by + // understanding minimum gas requirements from the spec + // Using provider.estimate_eip1559_fees() during broadcast + // Respecting --gas-* CLI arguments + tx_req.gas = Some(500_000); // Using 500k to provide safe headroom for complex calls + tx_req.max_fee_per_gas = Some(ecx.env.block.basefee.to()); // 3 gwei + tx_req.max_priority_fee_per_gas = Some(2_000_000_252); // 2 gwei priority + } + 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"); diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index e3fbeb3a4ebc7..49d5540c495db 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -63,6 +63,9 @@ impl Cheatcode for attachDelegationCall { authority_acc.info.code_hash = bytecode.hash_slow(); authority_acc.mark_touch(); + ccx.state.delegations.insert(*implementation, signed_auth); + ccx.state.active_delegation = Some(*implementation); + Ok(Default::default()) } } @@ -76,7 +79,6 @@ impl Cheatcode for signDelegationCall { 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(); From af7e39c4f40db22f2e4822e7be0aefb5c8b49137 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 7 Nov 2024 17:38:09 +0700 Subject: [PATCH 29/54] remove obsolete comment --- crates/cheatcodes/src/inspector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index a59cdbeef6414..df0b5e61e2898 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -1027,7 +1027,7 @@ where { // Using provider.estimate_eip1559_fees() during broadcast // Respecting --gas-* CLI arguments tx_req.gas = Some(500_000); // Using 500k to provide safe headroom for complex calls - tx_req.max_fee_per_gas = Some(ecx.env.block.basefee.to()); // 3 gwei + tx_req.max_fee_per_gas = Some(ecx.env.block.basefee.to()); tx_req.max_priority_fee_per_gas = Some(2_000_000_252); // 2 gwei priority } From 439406516793e0ddf4683ed335a123bdcd89249e Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 7 Nov 2024 19:04:52 +0700 Subject: [PATCH 30/54] add comments explaining delegations and active_delegation --- crates/cheatcodes/src/inspector.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index df0b5e61e2898..959e4d5b3d7d1 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -373,7 +373,11 @@ pub struct Cheatcodes { /// execution block environment. pub block: Option, + /// Mapping of addresses to their signed authorization data for EIP-7702 delegated transactions pub delegations: HashMap, + /// The currently active delegation address that will be used for the next transaction. + /// When a delegation is attached via `vm.attachDelegation()`, this address is set and used + /// to look up the authorization data from the `delegations` map when broadcasting transactions. pub active_delegation: Option
, /// The gas price. From aa0ece74f0fb8c13dbc800a225d821c4d4d49794 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 7 Nov 2024 19:11:31 +0700 Subject: [PATCH 31/54] cargo fmt --- crates/cheatcodes/src/inspector.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 959e4d5b3d7d1..934c17ce98c38 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -46,7 +46,10 @@ use revm::{ EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, }, - primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId, EOF_MAGIC_BYTES}, + primitives::{ + BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SignedAuthorization, SpecId, + EOF_MAGIC_BYTES, + }, EvmContext, InnerEvmContext, Inspector, }; use serde_json::Value; @@ -377,7 +380,8 @@ pub struct Cheatcodes { pub delegations: HashMap, /// The currently active delegation address that will be used for the next transaction. /// When a delegation is attached via `vm.attachDelegation()`, this address is set and used - /// to look up the authorization data from the `delegations` map when broadcasting transactions. + /// to look up the authorization data from the `delegations` map when broadcasting + /// transactions. pub active_delegation: Option
, /// The gas price. @@ -1021,18 +1025,21 @@ where { ..Default::default() }; - if let Some(auth_list) = self.active_delegation.and_then(|addr| self.delegations.remove(&addr)) { + if let Some(auth_list) = + self.active_delegation.and_then(|addr| self.delegations.remove(&addr)) + { tx_req.authorization_list = Some(vec![auth_list]); tx_req.transaction_type = Some(4); // EIP-7702 - // known working values for 7702 on odyssey - // TODO(7702): These gas values are temporary defaults that work for testing. - // We should properly handle gas estimation for EIP-7702 transactions by - // understanding minimum gas requirements from the spec - // Using provider.estimate_eip1559_fees() during broadcast - // Respecting --gas-* CLI arguments - tx_req.gas = Some(500_000); // Using 500k to provide safe headroom for complex calls + // known working values for 7702 on odyssey + // TODO(7702): These gas values are temporary defaults that work for + // testing. + // We should properly handle gas estimation for EIP-7702 transactions by + // understanding minimum gas requirements from the spec + // Using provider.estimate_eip1559_fees() during broadcast + // Respecting --gas-* CLI arguments + tx_req.gas = Some(500_000); // Using 500k to provide safe headroom for complex calls tx_req.max_fee_per_gas = Some(ecx.env.block.basefee.to()); - tx_req.max_priority_fee_per_gas = Some(2_000_000_252); // 2 gwei priority + tx_req.max_priority_fee_per_gas = Some(2_000_000_252); // 2 gwei priority } self.broadcastable_transactions.push_back(BroadcastableTransaction { From a833dd6468b158bf15c06b7c1d47ffe88a30f413 Mon Sep 17 00:00:00 2001 From: evchip Date: Fri, 8 Nov 2024 13:12:09 +0700 Subject: [PATCH 32/54] 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 --- crates/cheatcodes/src/inspector.rs | 10 ---------- crates/script/src/broadcast.rs | 15 +++++++++++---- crates/script/src/transaction.rs | 8 +++++++- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 934c17ce98c38..14738a16750fd 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -1030,16 +1030,6 @@ where { { tx_req.authorization_list = Some(vec![auth_list]); tx_req.transaction_type = Some(4); // EIP-7702 - // known working values for 7702 on odyssey - // TODO(7702): These gas values are temporary defaults that work for - // testing. - // We should properly handle gas estimation for EIP-7702 transactions by - // understanding minimum gas requirements from the spec - // Using provider.estimate_eip1559_fees() during broadcast - // Respecting --gas-* CLI arguments - tx_req.gas = Some(500_000); // Using 500k to provide safe headroom for complex calls - tx_req.max_fee_per_gas = Some(ecx.env.block.basefee.to()); - tx_req.max_priority_fee_per_gas = Some(2_000_000_252); // 2 gwei priority } self.broadcastable_transactions.push_back(BroadcastableTransaction { diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 4058aa6c59b3f..a66e45734caed 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -4,7 +4,7 @@ use crate::{ }; use alloy_chains::Chain; use alloy_consensus::TxEnvelope; -use alloy_eips::eip2718::Encodable2718; +use alloy_eips::{eip2718::Encodable2718, eip7702::constants::PER_EMPTY_ACCOUNT_COST}; use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder}; use alloy_primitives::{ map::{AddressHashMap, AddressHashSet}, @@ -41,11 +41,18 @@ where // set in the request and omit the estimate altogether, so we remove it here tx.gas = None; - tx.set_gas_limit( + let auth_list_cost = tx + .authorization_list + .as_ref() + .map(|list| list.len() as u64 * PER_EMPTY_ACCOUNT_COST) + .unwrap_or(0); + // estimate normal execution cost + let execution_cost = provider.estimate_gas(tx).await.wrap_err("Failed to estimate gas for tx")? * estimate_multiplier / - 100, - ); + 100; + + tx.set_gas_limit(execution_cost + auth_list_cost); Ok(()) } diff --git a/crates/script/src/transaction.rs b/crates/script/src/transaction.rs index fcd4eefc36d4b..b41b297322a49 100644 --- a/crates/script/src/transaction.rs +++ b/crates/script/src/transaction.rs @@ -1,5 +1,6 @@ use super::ScriptResult; use alloy_dyn_abi::JsonAbiExt; +use alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST; use alloy_primitives::{hex, Address, TxKind, B256}; use eyre::Result; use forge_script_sequence::TransactionWithMetadata; @@ -159,8 +160,13 @@ impl ScriptTransactionBuilder { if !self.transaction.is_fixed_gas_limit { if let Some(unsigned) = self.transaction.transaction.as_unsigned_mut() { + let mut gas = result.gas_used; + // if this is a 7702 tx, add the per-account auth costs + if let Some(auth_list) = unsigned.authorization_list.as_ref() { + gas += auth_list.len() as u64 * PER_EMPTY_ACCOUNT_COST; + } // We inflate the gas used by the user specified percentage - unsigned.gas = Some(result.gas_used * gas_estimate_multiplier / 100); + unsigned.gas = Some(gas * gas_estimate_multiplier / 100); } } From f432430a41fd34d2afad447afadfbec40928cbcd Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 10 Nov 2024 12:20:28 +0700 Subject: [PATCH 33/54] revert logic to add PER_EMPTY_ACCOUNT_COST for EIP 7702 txs - handled inside of revm now --- crates/script/src/broadcast.rs | 15 ++++----------- crates/script/src/transaction.rs | 8 +------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index a66e45734caed..4058aa6c59b3f 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -4,7 +4,7 @@ use crate::{ }; use alloy_chains::Chain; use alloy_consensus::TxEnvelope; -use alloy_eips::{eip2718::Encodable2718, eip7702::constants::PER_EMPTY_ACCOUNT_COST}; +use alloy_eips::eip2718::Encodable2718; use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder}; use alloy_primitives::{ map::{AddressHashMap, AddressHashSet}, @@ -41,18 +41,11 @@ where // set in the request and omit the estimate altogether, so we remove it here tx.gas = None; - let auth_list_cost = tx - .authorization_list - .as_ref() - .map(|list| list.len() as u64 * PER_EMPTY_ACCOUNT_COST) - .unwrap_or(0); - // estimate normal execution cost - let execution_cost = + tx.set_gas_limit( provider.estimate_gas(tx).await.wrap_err("Failed to estimate gas for tx")? * estimate_multiplier / - 100; - - tx.set_gas_limit(execution_cost + auth_list_cost); + 100, + ); Ok(()) } diff --git a/crates/script/src/transaction.rs b/crates/script/src/transaction.rs index b41b297322a49..fcd4eefc36d4b 100644 --- a/crates/script/src/transaction.rs +++ b/crates/script/src/transaction.rs @@ -1,6 +1,5 @@ use super::ScriptResult; use alloy_dyn_abi::JsonAbiExt; -use alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST; use alloy_primitives::{hex, Address, TxKind, B256}; use eyre::Result; use forge_script_sequence::TransactionWithMetadata; @@ -160,13 +159,8 @@ impl ScriptTransactionBuilder { if !self.transaction.is_fixed_gas_limit { if let Some(unsigned) = self.transaction.transaction.as_unsigned_mut() { - let mut gas = result.gas_used; - // if this is a 7702 tx, add the per-account auth costs - if let Some(auth_list) = unsigned.authorization_list.as_ref() { - gas += auth_list.len() as u64 * PER_EMPTY_ACCOUNT_COST; - } // We inflate the gas used by the user specified percentage - unsigned.gas = Some(gas * gas_estimate_multiplier / 100); + unsigned.gas = Some(result.gas_used * gas_estimate_multiplier / 100); } } From 6d464175b04dded4767d31ca0fb29d2838cdd29b Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 10 Nov 2024 12:21:03 +0700 Subject: [PATCH 34/54] remove manually setting transaction type to 4 if auth list is present - handled in revm --- crates/cheatcodes/src/inspector.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 14738a16750fd..376ac3b10fabd 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -1029,7 +1029,6 @@ where { self.active_delegation.and_then(|addr| self.delegations.remove(&addr)) { tx_req.authorization_list = Some(vec![auth_list]); - tx_req.transaction_type = Some(4); // EIP-7702 } self.broadcastable_transactions.push_back(BroadcastableTransaction { From d79f3764634d67d37a2431a181dfbb27e223449f Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 10 Nov 2024 12:22:49 +0700 Subject: [PATCH 35/54] 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 --- crates/evm/evm/src/executors/mod.rs | 14 ++++++++++++-- crates/script/src/simulate.rs | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 723b520aa1b71..bd1d1c9fff625 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -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; @@ -201,6 +201,16 @@ impl Executor { Ok(self.backend().basic_ref(address)?.map(|acc| acc.balance).unwrap_or_default()) } + /// Sets EIP-7702 authorization list in the transaction environment. + pub fn set_delegation( + &mut self, + authorization_list: &[SignedAuthorization], + ) -> BackendResult<()> { + self.env.tx.authorization_list = + Some(AuthorizationList::Signed(authorization_list.to_vec())); + Ok(()) + } + /// Set the nonce of an account. pub fn set_nonce(&mut self, address: Address, nonce: u64) -> BackendResult<()> { let mut account = self.backend().basic_ref(address)?.unwrap_or_default(); diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 6eb11d7f91176..92467357a1329 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -115,6 +115,12 @@ impl PreSimulationState { .map(|mut transaction| async { let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); + if let Some(tx) = transaction.tx_mut().as_unsigned_mut() { + if let Some(authorization_list) = &tx.authorization_list { + runner.executor.set_delegation(authorization_list)?; + } + } + let tx = transaction.tx_mut(); let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; let result = runner From c0503934c29579290cdb1ce194ca82400ce428d9 Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 10 Nov 2024 12:41:56 +0700 Subject: [PATCH 36/54] remove redundancy with TransactionMaybeSigned var tx --- crates/script/src/simulate.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index e1fedf8c0f63c..140fa5aa3ae4a 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -112,16 +112,16 @@ impl PreSimulationState { // Executes all transactions from the different forks concurrently. let futs = transactions .into_iter() - .map(|transaction| async { - let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); + .map(|mut transaction| async { + let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); + let tx = transaction.tx_mut(); - if let Some(tx) = transaction.tx_mut().as_unsigned_mut() { + if let Some(tx) = tx.as_unsigned_mut() { if let Some(authorization_list) = &tx.authorization_list { runner.executor.set_delegation(authorization_list)?; } } - let tx = transaction.tx(); let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; let result = runner .simulate( From cdfe50330b69362e7022f67f7fac662bd83ab899 Mon Sep 17 00:00:00 2001 From: evchip Date: Sun, 10 Nov 2024 12:48:20 +0700 Subject: [PATCH 37/54] cargo fmt --- crates/script/src/simulate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 140fa5aa3ae4a..651d05020761c 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -113,7 +113,7 @@ impl PreSimulationState { let futs = transactions .into_iter() .map(|mut transaction| async { - let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); + let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); let tx = transaction.tx_mut(); if let Some(tx) = tx.as_unsigned_mut() { From 475216d6827f2612ec80e6579138a666380fb3af Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 11 Nov 2024 12:49:28 +0700 Subject: [PATCH 38/54] refactor: use authorization_list() helper to return authorization_list and set delegation --- crates/common/src/transactions.rs | 9 +++++++++ crates/script/src/simulate.rs | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/common/src/transactions.rs b/crates/common/src/transactions.rs index 4ddef26dd03f9..f0ed0b54f8bda 100644 --- a/crates/common/src/transactions.rs +++ b/crates/common/src/transactions.rs @@ -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}, @@ -226,6 +227,14 @@ impl TransactionMaybeSigned { Self::Unsigned(tx) => tx.nonce, } } + + pub fn authorization_list(&self) -> Option> { + 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 for TransactionMaybeSigned { diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 651d05020761c..2e8c649040b7c 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -116,10 +116,8 @@ impl PreSimulationState { let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); let tx = transaction.tx_mut(); - if let Some(tx) = tx.as_unsigned_mut() { - if let Some(authorization_list) = &tx.authorization_list { - runner.executor.set_delegation(authorization_list)?; - } + if let Some(authorization_list) = tx.authorization_list() { + runner.executor.set_delegation(&authorization_list)?; } let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; From d0ad8bddbb7c8c245e500090709599958bb1211f Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 11 Nov 2024 12:52:46 +0700 Subject: [PATCH 39/54] refactor: change Cheatcodes::active_delegation to Option and remove delegations hashmap - tx will only use one active delegation at a time, so no need for mapping --- crates/cheatcodes/src/inspector.rs | 15 ++++----------- crates/cheatcodes/src/script.rs | 3 +-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 376ac3b10fabd..c625ad7432f18 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -376,13 +376,9 @@ pub struct Cheatcodes { /// execution block environment. pub block: Option, - /// Mapping of addresses to their signed authorization data for EIP-7702 delegated transactions - pub delegations: HashMap, - /// The currently active delegation address that will be used for the next transaction. - /// When a delegation is attached via `vm.attachDelegation()`, this address is set and used - /// to look up the authorization data from the `delegations` map when broadcasting - /// transactions. - pub active_delegation: Option
, + /// 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, /// The gas price. /// @@ -508,7 +504,6 @@ impl Cheatcodes { labels: config.labels.clone(), config, block: Default::default(), - delegations: Default::default(), active_delegation: Default::default(), gas_price: Default::default(), prank: Default::default(), @@ -1025,9 +1020,7 @@ where { ..Default::default() }; - if let Some(auth_list) = - self.active_delegation.and_then(|addr| self.delegations.remove(&addr)) - { + if let Some(auth_list) = self.active_delegation.take() { tx_req.authorization_list = Some(vec![auth_list]); } diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 49d5540c495db..c32cead1c1e1b 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -63,8 +63,7 @@ impl Cheatcode for attachDelegationCall { authority_acc.info.code_hash = bytecode.hash_slow(); authority_acc.mark_touch(); - ccx.state.delegations.insert(*implementation, signed_auth); - ccx.state.active_delegation = Some(*implementation); + ccx.state.active_delegation = Some(signed_auth); Ok(Default::default()) } From 113854cce2a055332912dd4e4137bc3d8ab3002f Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 12 Nov 2024 12:33:12 +0700 Subject: [PATCH 40/54] replace verbose logic to set bytecode on EOA with journaled_state.set_code helper --- crates/cheatcodes/src/script.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index c32cead1c1e1b..2a9a4a0df2e56 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -36,8 +36,7 @@ impl Cheatcode for attachDelegationCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { implementation, authority, v, r, s } = self; - let mut authority_acc = - ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?; + let authority_acc = ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?; let auth = Authorization { address: *implementation, @@ -59,9 +58,7 @@ impl Cheatcode for attachDelegationCall { // write delegation code let bytecode = Bytecode::new_eip7702(*implementation); - authority_acc.info.code = Some(bytecode.clone()); - authority_acc.info.code_hash = bytecode.hash_slow(); - authority_acc.mark_touch(); + ccx.ecx.journaled_state.set_code(*authority, bytecode); ccx.state.active_delegation = Some(signed_auth); From 0a939894493200dd4d4100577cebe8a117a2be76 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 12 Nov 2024 12:33:25 +0700 Subject: [PATCH 41/54] cargo fmt --- crates/cheatcodes/src/inspector.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index c625ad7432f18..789266e0112e5 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -376,8 +376,9 @@ pub struct Cheatcodes { /// execution block environment. pub block: Option, - /// 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. + /// 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, /// The gas price. From 74302de71e2e2e49f9f83f2017a86396fc8eaca7 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 12 Nov 2024 12:43:28 +0700 Subject: [PATCH 42/54] increment nonce of authority account --- crates/cheatcodes/src/script.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 2a9a4a0df2e56..07db4ce6877ed 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -56,7 +56,7 @@ impl Cheatcode for attachDelegationCall { return Err("invalid signature".into()); } - // write delegation code + authority_acc.data.info.nonce += 1; let bytecode = Bytecode::new_eip7702(*implementation); ccx.ecx.journaled_state.set_code(*authority, bytecode); From de9dea45070279e29253af3638519245178ed9b0 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 12 Nov 2024 16:15:46 +0700 Subject: [PATCH 43/54] add logic to set authorization_list to None if active_delegation is None --- crates/cheatcodes/src/inspector.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 789266e0112e5..e72cc4b37d309 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -1023,6 +1023,8 @@ where { 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 { From d560464f27e16cde3564c186ae6184643cd8af05 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 12 Nov 2024 16:17:00 +0700 Subject: [PATCH 44/54] add test testSwitchDelegation to assert that attaching an additional delegation switches the implementation on the EOA --- .../default/cheats/AttachDelegation.t.sol | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index 6fd3f6416a415..f9d45214a2ff7 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -5,6 +5,8 @@ import "ds-test/test.sol"; import "cheats/Vm.sol"; contract AttachDelegationTest is DSTest { + event ExecutedBy(uint256 id); + Vm constant vm = Vm(HEVM_ADDRESS); uint256 alice_pk = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; address payable alice = payable(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); @@ -12,10 +14,12 @@ contract AttachDelegationTest is DSTest { address bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; SimpleDelegateContract implementation; + SimpleDelegateContract implementation2; ERC20 token; function setUp() public { - implementation = new SimpleDelegateContract(); + implementation = new SimpleDelegateContract(1); + implementation2 = new SimpleDelegateContract(2); token = new ERC20(alice); } @@ -62,6 +66,37 @@ contract AttachDelegationTest is DSTest { assertEq(token.balanceOf(address(this)), 50); } + function testSwitchDelegation() public { + (uint8 v1, bytes32 r1, bytes32 s1) = vm.signDelegation(address(implementation), alice_pk); + + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); + bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); + calls[0] = SimpleDelegateContract.Call({ + to: address(token), + data: data, + value: 0 + }); + + vm.broadcast(bob_pk); + vm.attachDelegation(address(implementation), alice, v1, r1, s1); + + vm.expectEmit(true, true, true, true); + emit ExecutedBy(1); + SimpleDelegateContract(alice).execute(calls); + + // switch to implementation2 + (uint8 v2, bytes32 r2, bytes32 s2) = vm.signDelegation(address(implementation2), alice_pk); + vm.broadcast(bob_pk); + vm.attachDelegation(address(implementation2), alice, v2, r2, s2); + + vm.expectEmit(true, true, true, true); + emit ExecutedBy(2); + SimpleDelegateContract(alice).execute(calls); + + // verify final state + assertEq(token.balanceOf(bob), 200); + } + function testAttachDelegationRevertInvalidSignature() public { (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); // change v from 1 to 0 @@ -85,18 +120,26 @@ contract AttachDelegationTest is DSTest { contract SimpleDelegateContract { event Executed(address indexed to, uint256 value, bytes data); + event ExecutedBy(uint256 id); struct Call { bytes data; address to; uint256 value; } + uint256 public immutable id; + + constructor(uint256 _id) { + id = _id; + } + function execute(Call[] memory calls) external payable { for (uint256 i = 0; i < calls.length; i++) { Call memory call = calls[i]; (bool success, bytes memory result) = call.to.call{value: call.value}(call.data); require(success, string(result)); emit Executed(call.to, call.value, call.data); + emit ExecutedBy(id); } } From 2a30b429a2fa8f2a9e32277374f2bcb4502a2c09 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 14 Nov 2024 11:53:39 +0700 Subject: [PATCH 45/54] 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 --- crates/common/Cargo.toml | 1 + crates/evm/evm/src/executors/mod.rs | 25 +++++++++++++++---------- crates/script/src/runner.rs | 26 +++++++++++++++++++++++--- crates/script/src/simulate.rs | 5 +---- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index af95e94bcc35a..c2da9862c982d 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -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 = [ diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index bd1d1c9fff625..21b06823017dd 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -201,16 +201,6 @@ impl Executor { Ok(self.backend().basic_ref(address)?.map(|acc| acc.balance).unwrap_or_default()) } - /// Sets EIP-7702 authorization list in the transaction environment. - pub fn set_delegation( - &mut self, - authorization_list: &[SignedAuthorization], - ) -> BackendResult<()> { - self.env.tx.authorization_list = - Some(AuthorizationList::Signed(authorization_list.to_vec())); - Ok(()) - } - /// Set the nonce of an account. pub fn set_nonce(&mut self, address: Address, nonce: u64) -> BackendResult<()> { let mut account = self.backend().basic_ref(address)?.unwrap_or_default(); @@ -388,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, + ) -> eyre::Result { + 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, diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index a7d950690b92d..fa8ff19d9a9dc 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -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; @@ -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 { - 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. @@ -233,9 +234,17 @@ impl ScriptRunner { to: Option
, calldata: Option, value: Option, + authorization_list: Option>, ) -> Result { 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, @@ -282,9 +291,20 @@ impl ScriptRunner { to: Address, calldata: Bytes, value: U256, + authorization_list: Option>, commit: bool, ) -> Result { - 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 diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 2e8c649040b7c..ed00a29a472eb 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -116,10 +116,6 @@ impl PreSimulationState { let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); let tx = transaction.tx_mut(); - if let Some(authorization_list) = tx.authorization_list() { - runner.executor.set_delegation(&authorization_list)?; - } - let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; let result = runner .simulate( @@ -128,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")?; From 713bd648011eab57e01c94df75033a97902ff602 Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 18 Nov 2024 15:28:30 +0700 Subject: [PATCH 46/54] refactor signDelegation to return struct SignedDelegation and for attachDelegation to accept SignedDelegation --- crates/cheatcodes/assets/cheatcodes.json | 16 +++++----- crates/cheatcodes/spec/src/vm.rs | 12 +++++-- crates/cheatcodes/src/script.rs | 40 ++++++++++++++++-------- testdata/cheats/Vm.sol | 11 +++++-- 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 407797b1830e3..1c3c8cf0d283a 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -3080,16 +3080,16 @@ "func": { "id": "attachDelegation", "description": "Designate the next call as an EIP-7702 transaction", - "declaration": "function attachDelegation(address implementation, address authority, uint8 v, bytes32 r, bytes32 s) external;", + "declaration": "function attachDelegation(SignedDelegation memory signedDelegation) external;", "visibility": "external", "mutability": "", - "signature": "attachDelegation(address,address,uint8,bytes32,bytes32)", - "selector": "0x9c509458", + "signature": "attachDelegation((uint8,bytes32,bytes32,uint64,address))", + "selector": "0x14ae3519", "selectorBytes": [ - 156, - 80, - 148, - 88 + 20, + 174, + 53, + 25 ] }, "group": "scripting", @@ -8870,7 +8870,7 @@ "func": { "id": "signDelegation", "description": "Sign an EIP-7702 authorization for delegation", - "declaration": "function signDelegation(address implementation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s);", + "declaration": "function signDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation);", "visibility": "external", "mutability": "", "signature": "signDelegation(address,uint256)", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 2e80441c44118..d7bdf4bdfabce 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -308,6 +308,14 @@ interface Vm { bool success; } + struct SignedDelegation { + uint8 v; + bytes32 r; + bytes32 s; + uint64 nonce; + address implementation; + } + // ======== EVM ======== /// Gets the address for a given private key. @@ -2004,11 +2012,11 @@ interface Vm { /// Sign an EIP-7702 authorization for delegation #[cheatcode(group = Scripting)] - function signDelegation(address implementation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); + 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(address implementation, address authority, uint8 v, bytes32 r, bytes32 s) external; + function attachDelegation(SignedDelegation memory signedDelegation) external; /// Returns addresses of available unlocked wallets in the script environment. #[cheatcode(group = Scripting)] diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 07db4ce6877ed..7de16207377c2 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -1,7 +1,7 @@ //! Implementations of [`Scripting`](spec::Group::Scripting) cheatcodes. use crate::{Cheatcode, CheatsCtxt, Result, Vm::*}; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{Address, Uint, B256, U256}; use alloy_rpc_types::Authorization; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; @@ -34,13 +34,18 @@ 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; - - let authority_acc = ccx.ecx.journaled_state.load_account(*authority, &mut ccx.ecx.db)?; - + let Self { signedDelegation } = self; + let SignedDelegation { + v, + r, + s, + nonce, + implementation, + } = signedDelegation; + let auth = Authorization { address: *implementation, - nonce: authority_acc.data.info.nonce, + nonce: *nonce, chain_id: ccx.ecx.env.cfg.chain_id, }; let signed_auth = SignedAuthorization::new_unchecked( @@ -51,14 +56,15 @@ impl Cheatcode for attachDelegationCall { ); // 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()); + let recovered_authority: Address = signed_auth.recover_authority().map_err(|e| format!("{e}"))?; + let authority_acc = ccx.ecx.journaled_state.load_account(recovered_authority, &mut ccx.ecx.db)?; + if &authority_acc.data.info.nonce != nonce { + return Err("invalid nonce".into()); } authority_acc.data.info.nonce += 1; let bytecode = Bytecode::new_eip7702(*implementation); - ccx.ecx.journaled_state.set_code(*authority, bytecode); + ccx.ecx.journaled_state.set_code(recovered_authority, bytecode); ccx.state.active_delegation = Some(signed_auth); @@ -80,15 +86,23 @@ impl Cheatcode for signDelegationCall { let hash = auth.signature_hash(); let signer = super::crypto::parse_wallet(privateKey)?; let sig = signer.sign_hash_sync(&hash)?; - Ok(encode_delegation_sig(sig)) + let (v, r, s) = encode_delegation_sig(sig); + let signed_delegation = SignedDelegation { + v: v.try_into().unwrap(), + r: r.into(), + s: s.into(), + nonce, + implementation: *implementation + }; + Ok(signed_delegation.abi_encode()) } } -fn encode_delegation_sig(sig: alloy_primitives::Signature) -> Vec { +fn encode_delegation_sig(sig: alloy_primitives::Signature) -> (Uint<256, 4>, Uint<256, 4>, Uint<256, 4>) { let v = U256::from(sig.v().y_parity() as u8); let r = sig.r(); let s = sig.s(); - (v, r, s).abi_encode() + (v, r, s) } impl Cheatcode for startBroadcast_0Call { diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index a55fe6ff79964..d6fe89d7503e9 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -23,6 +23,13 @@ interface Vm { struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } struct DebugStep { uint256[] stack; bytes memoryInput; uint8 opcode; uint64 depth; bool isOutOfGas; address contractAddr; } struct BroadcastTxSummary { bytes32 txHash; BroadcastTxType txType; address contractAddress; uint64 blockNumber; bool success; } + struct SignedDelegation { + uint8 v; + bytes32 r; + bytes32 s; + uint64 nonce; + address implementation; + } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external; @@ -148,7 +155,7 @@ interface Vm { function assertTrue(bool condition, string calldata error) external pure; function assume(bool condition) external pure; function assumeNoRevert() external pure; - function attachDelegation(address implementation, address authority, uint8 v, bytes32 r, bytes32 s) external; + function attachDelegation(SignedDelegation memory signedDelegation) external; function blobBaseFee(uint256 newBlobBaseFee) external; function blobhashes(bytes32[] calldata hashes) external; function breakpoint(string calldata char) external pure; @@ -437,7 +444,7 @@ interface Vm { function signCompact(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(address signer, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); - function signDelegation(address implementation, uint256 privateKey) external returns (uint8 v, bytes32 r, bytes32 s); + function signDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation); function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); function sign(Wallet calldata wallet, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); From 89cd62d18ecddc76ce6ad3bc5863375f76404bda Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 18 Nov 2024 15:29:04 +0700 Subject: [PATCH 47/54] update delegation tests to reflect change in cheatcode interface for signDelegation and attachDelegation --- .../default/cheats/AttachDelegation.t.sol | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index f9d45214a2ff7..a17bcd8245737 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -24,7 +24,7 @@ contract AttachDelegationTest is DSTest { } function testCallSingleDelegation() public { - (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); calls[0] = SimpleDelegateContract.Call({ @@ -34,7 +34,7 @@ contract AttachDelegationTest is DSTest { }); // executing as bob to make clear that we don't need to execute the tx as alice vm.broadcast(bob_pk); - vm.attachDelegation(address(implementation), alice, v, r, s); + vm.attachDelegation(signedDelegation); bytes memory code = address(alice).code; require(code.length > 0, "no code written to alice"); @@ -44,9 +44,9 @@ contract AttachDelegationTest is DSTest { } function testMultiCallDelegation() public { - (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); vm.broadcast(bob_pk); - vm.attachDelegation(address(implementation), alice, v, r, s); + vm.attachDelegation(signedDelegation); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](2); calls[0] = SimpleDelegateContract.Call({ @@ -67,7 +67,7 @@ contract AttachDelegationTest is DSTest { } function testSwitchDelegation() public { - (uint8 v1, bytes32 r1, bytes32 s1) = vm.signDelegation(address(implementation), alice_pk); + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); @@ -78,16 +78,16 @@ contract AttachDelegationTest is DSTest { }); vm.broadcast(bob_pk); - vm.attachDelegation(address(implementation), alice, v1, r1, s1); + vm.attachDelegation(signedDelegation); vm.expectEmit(true, true, true, true); emit ExecutedBy(1); SimpleDelegateContract(alice).execute(calls); // switch to implementation2 - (uint8 v2, bytes32 r2, bytes32 s2) = vm.signDelegation(address(implementation2), alice_pk); + Vm.SignedDelegation memory signedDelegation2 = vm.signDelegation(address(implementation2), alice_pk); vm.broadcast(bob_pk); - vm.attachDelegation(address(implementation2), alice, v2, r2, s2); + vm.attachDelegation(signedDelegation2); vm.expectEmit(true, true, true, true); emit ExecutedBy(2); @@ -98,23 +98,34 @@ contract AttachDelegationTest is DSTest { } function testAttachDelegationRevertInvalidSignature() public { - (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); // change v from 1 to 0 - v = 0; + signedDelegation.v = 0; + vm.attachDelegation(signedDelegation); + + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); + bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); + calls[0] = SimpleDelegateContract.Call({ + to: address(token), + data: data, + value: 0 + }); - vm.expectRevert("vm.attachDelegation: invalid signature"); - vm.attachDelegation(address(implementation), alice, v, r, s); + vm.broadcast(alice_pk); + // empty revert because no bytecode was set to Alice's account + vm.expectRevert(); + SimpleDelegateContract(alice).execute(calls); } function testDelegationRevertsAfterNonceChange() public { - (uint8 v, bytes32 r, bytes32 s) = vm.signDelegation(address(implementation), alice_pk); + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); vm.broadcast(alice_pk); // send tx to increment alice's nonce token.mint(1, bob); - vm.expectRevert("vm.attachDelegation: invalid signature"); - vm.attachDelegation(address(implementation), alice, v, r, s); + vm.expectRevert("vm.attachDelegation: invalid nonce"); + vm.attachDelegation(signedDelegation); } } From 7df7dcbbda0285dd1f99feafefccf08e1dc402ae Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 18 Nov 2024 20:48:38 +0700 Subject: [PATCH 48/54] add cheatcode signAndAttachDelegation --- crates/cheatcodes/assets/cheatcodes.json | 20 ++++++++++++++++++++ crates/cheatcodes/spec/src/vm.rs | 4 ++++ testdata/cheats/Vm.sol | 9 ++------- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 1c3c8cf0d283a..dd0f3de3caf7c 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -8786,6 +8786,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "signAndAttachDelegation", + "description": "Sign an EIP-7702 authorization and designate the next call as an EIP-7702 transaction", + "declaration": "function signAndAttachDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation);", + "visibility": "external", + "mutability": "", + "signature": "signAndAttachDelegation(address,uint256)", + "selector": "0xc7fa7288", + "selectorBytes": [ + 199, + 250, + 114, + 136 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "signCompact_0", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index d7bdf4bdfabce..abe014ea7f97e 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2018,6 +2018,10 @@ interface Vm { #[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); diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index d6fe89d7503e9..43e733c8a238f 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -23,13 +23,7 @@ interface Vm { struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } struct DebugStep { uint256[] stack; bytes memoryInput; uint8 opcode; uint64 depth; bool isOutOfGas; address contractAddr; } struct BroadcastTxSummary { bytes32 txHash; BroadcastTxType txType; address contractAddress; uint64 blockNumber; bool success; } - struct SignedDelegation { - uint8 v; - bytes32 r; - bytes32 s; - uint64 nonce; - address implementation; - } + struct SignedDelegation { uint8 v; bytes32 r; bytes32 s; uint64 nonce; address implementation; } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external; @@ -440,6 +434,7 @@ interface Vm { function setEnv(string calldata name, string calldata value) external; function setNonce(address account, uint64 newNonce) external; function setNonceUnsafe(address account, uint64 newNonce) external; + function signAndAttachDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation); function signCompact(Wallet calldata wallet, bytes32 digest) external returns (bytes32 r, bytes32 vs); function signCompact(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 vs); function signCompact(bytes32 digest) external pure returns (bytes32 r, bytes32 vs); From 5151459e46e3491e5afbb31acfb449cbcba5db12 Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 18 Nov 2024 20:51:08 +0700 Subject: [PATCH 49/54] add signAndAttachDelegationCall cheatcode logic; refactor helper methods for shared logic used in 7702 delegation cheatcodes --- crates/cheatcodes/src/script.rs | 107 ++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 7de16207377c2..06fa09db2a84e 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -1,9 +1,9 @@ //! Implementations of [`Scripting`](spec::Group::Scripting) cheatcodes. use crate::{Cheatcode, CheatsCtxt, Result, Vm::*}; -use alloy_primitives::{Address, Uint, B256, U256}; +use alloy_primitives::{Address, B256, U256}; use alloy_rpc_types::Authorization; -use alloy_signer::SignerSync; +use alloy_signer::{Signature, SignerSync}; use alloy_signer_local::PrivateKeySigner; use alloy_sol_types::SolValue; use foundry_wallets::{multi_wallet::MultiWallet, WalletSigner}; @@ -35,13 +35,7 @@ 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 SignedDelegation { v, r, s, nonce, implementation } = signedDelegation; let auth = Authorization { address: *implementation, @@ -54,20 +48,8 @@ impl Cheatcode for attachDelegationCall { U256::from_be_bytes(r.0), U256::from_be_bytes(s.0), ); - - // verify signature is from claimed authority - let recovered_authority: Address = signed_auth.recover_authority().map_err(|e| format!("{e}"))?; - let authority_acc = ccx.ecx.journaled_state.load_account(recovered_authority, &mut ccx.ecx.db)?; - if &authority_acc.data.info.nonce != nonce { - return Err("invalid nonce".into()); - } - - authority_acc.data.info.nonce += 1; - let bytecode = Bytecode::new_eip7702(*implementation); - ccx.ecx.journaled_state.set_code(recovered_authority, bytecode); - + write_delegation(ccx, signed_auth.clone())?; ccx.state.active_delegation = Some(signed_auth); - Ok(Default::default()) } } @@ -75,34 +57,67 @@ impl Cheatcode for attachDelegationCall { 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> = - PrivateKeySigner::from_bytes(&key_bytes)?; + let signer = PrivateKeySigner::from_bytes(&B256::from(*privateKey))?; 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)?; - let (v, r, s) = encode_delegation_sig(sig); - let signed_delegation = SignedDelegation { - v: v.try_into().unwrap(), - r: r.into(), - s: s.into(), - nonce, - implementation: *implementation - }; - Ok(signed_delegation.abi_encode()) + 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 encode_delegation_sig(sig: alloy_primitives::Signature) -> (Uint<256, 4>, Uint<256, 4>, Uint<256, 4>) { - let v = U256::from(sig.v().y_parity() as u8); - let r = sig.r(); - let s = sig.s(); - (v, r, s) +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 { From 1c6e7d0ceaa1154c7f606ab58675ae8ad34ec690 Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 18 Nov 2024 20:51:48 +0700 Subject: [PATCH 50/54] add test testCallSingleSignAndAttachDelegation for new cheatcode signAndAttachDelegation --- .../default/cheats/AttachDelegation.t.sol | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index a17bcd8245737..45ce96a7360a5 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -23,7 +23,7 @@ contract AttachDelegationTest is DSTest { token = new ERC20(alice); } - function testCallSingleDelegation() public { + function testCallSingleAttachDelegation() public { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); @@ -43,7 +43,7 @@ contract AttachDelegationTest is DSTest { assertEq(token.balanceOf(bob), 100); } - function testMultiCallDelegation() public { + function testMultiCallAttachDelegation() public { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); vm.broadcast(bob_pk); vm.attachDelegation(signedDelegation); @@ -66,7 +66,7 @@ contract AttachDelegationTest is DSTest { assertEq(token.balanceOf(address(this)), 50); } - function testSwitchDelegation() public { + function testSwitchAttachDelegation() public { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); @@ -117,7 +117,7 @@ contract AttachDelegationTest is DSTest { SimpleDelegateContract(alice).execute(calls); } - function testDelegationRevertsAfterNonceChange() public { + function testAttachDelegationRevertsAfterNonceChange() public { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); vm.broadcast(alice_pk); @@ -127,6 +127,23 @@ contract AttachDelegationTest is DSTest { vm.expectRevert("vm.attachDelegation: invalid nonce"); vm.attachDelegation(signedDelegation); } + + function testCallSingleSignAndAttachDelegation() public { + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); + bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); + calls[0] = SimpleDelegateContract.Call({ + to: address(token), + data: data, + value: 0 + }); + vm.signAndAttachDelegation(address(implementation), alice_pk); + bytes memory code = address(alice).code; + require(code.length > 0, "no code written to alice"); + vm.broadcast(bob_pk); + SimpleDelegateContract(alice).execute(calls); + + assertEq(token.balanceOf(bob), 100); + } } contract SimpleDelegateContract { From ff2801c913c4ed81196d2c9e3f1b67482c80935c Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 18 Nov 2024 21:08:10 +0700 Subject: [PATCH 51/54] add comments to SignedDelegation struct and cargo fmt --- crates/cheatcodes/spec/src/vm.rs | 8 ++++++++ crates/cheatcodes/src/script.rs | 24 +++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index abe014ea7f97e..0bfa51506cbf4 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -308,11 +308,19 @@ 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; } diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 06fa09db2a84e..3e1239237e83f 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -36,7 +36,7 @@ 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, @@ -79,14 +79,17 @@ impl Cheatcode for signAndAttachDelegationCall { } } -fn create_auth(ccx: &mut CheatsCtxt, implementation: Address, authority: Address) -> Result<(Authorization, u64)> { +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, + Ok(( + Authorization { address: implementation, nonce, chain_id: ccx.ecx.env.cfg.chain_id }, nonce, - chain_id: ccx.ecx.env.cfg.chain_id - }, nonce)) + )) } fn write_delegation(ccx: &mut CheatsCtxt, auth: SignedAuthorization) -> Result<()> { @@ -107,17 +110,12 @@ fn sig_to_delegation(sig: Signature, nonce: u64, implementation: Address) -> Sig r: sig.r().into(), s: sig.s().into(), nonce, - implementation + implementation, } } fn sig_to_auth(sig: Signature, auth: Authorization) -> SignedAuthorization { - SignedAuthorization::new_unchecked( - auth, - sig.v().y_parity() as u8, - sig.r(), - sig.s() - ) + SignedAuthorization::new_unchecked(auth, sig.v().y_parity() as u8, sig.r(), sig.s()) } impl Cheatcode for startBroadcast_0Call { From 002221aef95f7abc8d3190e22de9bafc86cf4e20 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 19 Nov 2024 08:59:28 +0700 Subject: [PATCH 52/54] cargo fmt --- crates/cheatcodes/spec/src/vm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 0bfa51506cbf4..450baba69d306 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -308,7 +308,7 @@ interface Vm { bool success; } - /// Holds a signed EIP-7702 authorization for an authority account to delegate to an implementation. + /// 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; From e829b6e6b4127c17dc30cb8c6adb27a79ffe18ba Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 19 Nov 2024 13:49:23 +0400 Subject: [PATCH 53/54] fix ci --- Cargo.lock | 1 + testdata/cheats/Vm.sol | 1 - .../default/cheats/AttachDelegation.t.sol | 52 ++++++------------- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b18148fe967b2..43c31a4e2c496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3698,6 +3698,7 @@ dependencies = [ "alloy-consensus", "alloy-contract", "alloy-dyn-abi", + "alloy-eips", "alloy-json-abi", "alloy-json-rpc", "alloy-primitives", diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 43e733c8a238f..795eddbc8eeb2 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -23,7 +23,6 @@ interface Vm { struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } struct DebugStep { uint256[] stack; bytes memoryInput; uint8 opcode; uint64 depth; bool isOutOfGas; address contractAddr; } struct BroadcastTxSummary { bytes32 txHash; BroadcastTxType txType; address contractAddress; uint64 blockNumber; bool success; } - struct SignedDelegation { uint8 v; bytes32 r; bytes32 s; uint64 nonce; address implementation; } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external; diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index 45ce96a7360a5..7befc9a32047c 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -10,7 +10,7 @@ contract AttachDelegationTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); uint256 alice_pk = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; address payable alice = payable(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); - uint256 bob_pk=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; + uint256 bob_pk = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; address bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; SimpleDelegateContract implementation; @@ -27,11 +27,7 @@ contract AttachDelegationTest is DSTest { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); - calls[0] = SimpleDelegateContract.Call({ - to: address(token), - data: data, - value: 0 - }); + calls[0] = SimpleDelegateContract.Call({to: address(token), data: data, value: 0}); // executing as bob to make clear that we don't need to execute the tx as alice vm.broadcast(bob_pk); vm.attachDelegation(signedDelegation); @@ -49,13 +45,10 @@ contract AttachDelegationTest is DSTest { vm.attachDelegation(signedDelegation); SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](2); - calls[0] = SimpleDelegateContract.Call({ - to: address(token), - data: abi.encodeCall(ERC20.mint, (50, bob)), - value: 0 - }); + calls[0] = + SimpleDelegateContract.Call({to: address(token), data: abi.encodeCall(ERC20.mint, (50, bob)), value: 0}); calls[1] = SimpleDelegateContract.Call({ - to: address(token), + to: address(token), data: abi.encodeCall(ERC20.mint, (50, address(this))), value: 0 }); @@ -68,14 +61,10 @@ contract AttachDelegationTest is DSTest { function testSwitchAttachDelegation() public { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); - + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); - calls[0] = SimpleDelegateContract.Call({ - to: address(token), - data: data, - value: 0 - }); + calls[0] = SimpleDelegateContract.Call({to: address(token), data: data, value: 0}); vm.broadcast(bob_pk); vm.attachDelegation(signedDelegation); @@ -84,11 +73,11 @@ contract AttachDelegationTest is DSTest { emit ExecutedBy(1); SimpleDelegateContract(alice).execute(calls); - // switch to implementation2 + // switch to implementation2 Vm.SignedDelegation memory signedDelegation2 = vm.signDelegation(address(implementation2), alice_pk); vm.broadcast(bob_pk); vm.attachDelegation(signedDelegation2); - + vm.expectEmit(true, true, true, true); emit ExecutedBy(2); SimpleDelegateContract(alice).execute(calls); @@ -100,16 +89,12 @@ contract AttachDelegationTest is DSTest { function testAttachDelegationRevertInvalidSignature() public { Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), alice_pk); // change v from 1 to 0 - signedDelegation.v = 0; + signedDelegation.v = (signedDelegation.v + 1) % 2; vm.attachDelegation(signedDelegation); - + SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); - calls[0] = SimpleDelegateContract.Call({ - to: address(token), - data: data, - value: 0 - }); + calls[0] = SimpleDelegateContract.Call({to: address(token), data: data, value: 0}); vm.broadcast(alice_pk); // empty revert because no bytecode was set to Alice's account @@ -123,7 +108,7 @@ contract AttachDelegationTest is DSTest { vm.broadcast(alice_pk); // send tx to increment alice's nonce token.mint(1, bob); - + vm.expectRevert("vm.attachDelegation: invalid nonce"); vm.attachDelegation(signedDelegation); } @@ -131,11 +116,7 @@ contract AttachDelegationTest is DSTest { function testCallSingleSignAndAttachDelegation() public { SimpleDelegateContract.Call[] memory calls = new SimpleDelegateContract.Call[](1); bytes memory data = abi.encodeCall(ERC20.mint, (100, bob)); - calls[0] = SimpleDelegateContract.Call({ - to: address(token), - data: data, - value: 0 - }); + calls[0] = SimpleDelegateContract.Call({to: address(token), data: data, value: 0}); vm.signAndAttachDelegation(address(implementation), alice_pk); bytes memory code = address(alice).code; require(code.length > 0, "no code written to alice"); @@ -149,6 +130,7 @@ contract AttachDelegationTest is DSTest { contract SimpleDelegateContract { event Executed(address indexed to, uint256 value, bytes data); event ExecutedBy(uint256 id); + struct Call { bytes data; address to; @@ -156,7 +138,7 @@ contract SimpleDelegateContract { } uint256 public immutable id; - + constructor(uint256 _id) { id = _id; } @@ -197,4 +179,4 @@ contract ERC20 { _balances[account] += amount; } } -} \ No newline at end of file +} From d2d2376c4c50a6f37f5e355ec9882768370a634b Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 19 Nov 2024 14:34:12 +0400 Subject: [PATCH 54/54] fix spec --- crates/cheatcodes/assets/cheatcodes.json | 31 ++++++++++++++++++++++++ crates/cheatcodes/spec/src/lib.rs | 1 + testdata/cheats/Vm.sol | 1 + 3 files changed, 33 insertions(+) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index dd0f3de3caf7c..a303c11995fda 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -573,6 +573,37 @@ "description": "Status of the transaction, retrieved from the transaction receipt." } ] + }, + { + "name": "SignedDelegation", + "description": "Holds a signed EIP-7702 authorization for an authority account to delegate to an implementation.", + "fields": [ + { + "name": "v", + "ty": "uint8", + "description": "The y-parity of the recovered secp256k1 signature (0 or 1)." + }, + { + "name": "r", + "ty": "bytes32", + "description": "First 32 bytes of the signature." + }, + { + "name": "s", + "ty": "bytes32", + "description": "Second 32 bytes of the signature." + }, + { + "name": "nonce", + "ty": "uint64", + "description": "The current nonce of the authority account at signing time.\n Used to ensure signature can't be replayed after account nonce changes." + }, + { + "name": "implementation", + "ty": "address", + "description": "Address of the contract implementation that will be delegated to.\n Gets encoded into delegation code: 0xef0100 || implementation." + } + ] } ], "cheatcodes": [ diff --git a/crates/cheatcodes/spec/src/lib.rs b/crates/cheatcodes/spec/src/lib.rs index 5692dfc48b9cd..c4d7e9868fe13 100644 --- a/crates/cheatcodes/spec/src/lib.rs +++ b/crates/cheatcodes/spec/src/lib.rs @@ -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(), diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 795eddbc8eeb2..43e733c8a238f 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -23,6 +23,7 @@ interface Vm { struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } struct DebugStep { uint256[] stack; bytes memoryInput; uint8 opcode; uint64 depth; bool isOutOfGas; address contractAddr; } struct BroadcastTxSummary { bytes32 txHash; BroadcastTxType txType; address contractAddress; uint64 blockNumber; bool success; } + struct SignedDelegation { uint8 v; bytes32 r; bytes32 s; uint64 nonce; address implementation; } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external;