diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 50189117bd26a..723c01b3ef52c 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -625,6 +625,22 @@ "description": "The data to use to match encountered reverts" } ] + }, + { + "name": "AccessListItem", + "description": "An EIP-2930 access list item.", + "fields": [ + { + "name": "target", + "ty": "address", + "description": "The address to be added in access list." + }, + { + "name": "storageKeys", + "ty": "bytes32[]", + "description": "The storage keys to be added in access list." + } + ] } ], "cheatcodes": [ @@ -688,6 +704,26 @@ "status": "internal", "safety": "unsafe" }, + { + "func": { + "id": "accessList", + "description": "Utility cheatcode to set an EIP-2930 access list for all subsequent transactions.", + "declaration": "function accessList(AccessListItem[] calldata accessList) external;", + "visibility": "external", + "mutability": "", + "signature": "accessList((address,bytes32[])[])", + "selector": "0x743e4cb7", + "selectorBytes": [ + 116, + 62, + 76, + 183 + ] + }, + "group": "evm", + "status": "experimental", + "safety": "unsafe" + }, { "func": { "id": "accesses", @@ -3448,6 +3484,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "cold", + "description": "Utility cheatcode to mark specific storage slot as cold, simulating no prior read.", + "declaration": "function cold(address target, bytes32 slot) external;", + "visibility": "external", + "mutability": "", + "signature": "cold(address,bytes32)", + "selector": "0x40a4035f", + "selectorBytes": [ + 64, + 164, + 3, + 95 + ] + }, + "group": "evm", + "status": "experimental", + "safety": "unsafe" + }, { "func": { "id": "computeCreate2Address_0", @@ -6614,6 +6670,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "noAccessList", + "description": "Utility cheatcode to remove any EIP-2930 access list set by `accessList` cheatcode.", + "declaration": "function noAccessList() external;", + "visibility": "external", + "mutability": "", + "signature": "noAccessList()", + "selector": "0x238ad778", + "selectorBytes": [ + 35, + 138, + 215, + 120 + ] + }, + "group": "evm", + "status": "experimental", + "safety": "unsafe" + }, { "func": { "id": "parseAddress", @@ -10460,6 +10536,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "warm", + "description": "Utility cheatcode to mark specific storage slot as warm, simulating a prior read.", + "declaration": "function warm(address target, bytes32 slot) external;", + "visibility": "external", + "mutability": "", + "signature": "warm(address,bytes32)", + "selector": "0x289eb2d6", + "selectorBytes": [ + 40, + 158, + 178, + 214 + ] + }, + "group": "evm", + "status": "experimental", + "safety": "unsafe" + }, { "func": { "id": "warp", diff --git a/crates/cheatcodes/spec/src/lib.rs b/crates/cheatcodes/spec/src/lib.rs index 6dd6ee769f6b9..0a1e81c6f60d0 100644 --- a/crates/cheatcodes/spec/src/lib.rs +++ b/crates/cheatcodes/spec/src/lib.rs @@ -90,6 +90,7 @@ impl Cheatcodes<'static> { Vm::BroadcastTxSummary::STRUCT.clone(), Vm::SignedDelegation::STRUCT.clone(), Vm::PotentialRevert::STRUCT.clone(), + Vm::AccessListItem::STRUCT.clone(), ]), enums: Cow::Owned(vec![ Vm::CallerMode::ENUM.clone(), diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 1891cc45f4709..0b7cf87b2f7a2 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -223,6 +223,14 @@ interface Vm { bool reverted; } + /// An EIP-2930 access list item. + struct AccessListItem { + /// The address to be added in access list. + address target; + /// The storage keys to be added in access list. + bytes32[] storageKeys; + } + /// The result of a `stopAndReturnStateDiff` call. struct AccountAccess { /// The chain and fork the access occurred. @@ -544,6 +552,22 @@ interface Vm { #[cheatcode(group = Evm, safety = Unsafe, status = Experimental)] function cool(address target) external; + /// Utility cheatcode to set an EIP-2930 access list for all subsequent transactions. + #[cheatcode(group = Evm, safety = Unsafe, status = Experimental)] + function accessList(AccessListItem[] calldata accessList) external; + + /// Utility cheatcode to remove any EIP-2930 access list set by `accessList` cheatcode. + #[cheatcode(group = Evm, safety = Unsafe, status = Experimental)] + function noAccessList() external; + + /// Utility cheatcode to mark specific storage slot as warm, simulating a prior read. + #[cheatcode(group = Evm, safety = Unsafe, status = Experimental)] + function warm(address target, bytes32 slot) external; + + /// Utility cheatcode to mark specific storage slot as cold, simulating no prior read. + #[cheatcode(group = Evm, safety = Unsafe, status = Experimental)] + function cold(address target, bytes32 slot) external; + // -------- Call Manipulation -------- // --- Mocks --- diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index b7469ba8e6839..e4ae83698834a 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -16,6 +16,7 @@ use foundry_evm_core::{ constants::{CALLER, CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, TEST_CONTRACT_ADDRESS}, }; use foundry_evm_traces::StackSnapshotType; +use itertools::Itertools; use rand::Rng; use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY}; use std::{ @@ -585,6 +586,48 @@ impl Cheatcode for coolCall { } } +impl Cheatcode for accessListCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { accessList } = self; + let access_list = accessList + .iter() + .map(|item| { + let keys = item.storageKeys.iter().map(|key| B256::from(*key)).collect_vec(); + alloy_rpc_types::AccessListItem { address: item.target, storage_keys: keys } + }) + .collect_vec(); + state.access_list = Some(alloy_rpc_types::AccessList::from(access_list)); + Ok(Default::default()) + } +} + +impl Cheatcode for noAccessListCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self {} = self; + // Set to empty option in order to override previous applied access list. + if state.access_list.is_some() { + state.access_list = Some(alloy_rpc_types::AccessList::default()); + } + Ok(Default::default()) + } +} + +impl Cheatcode for warmCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { target, slot } = *self; + set_cold_slot(ccx, target, slot.into(), false); + Ok(Default::default()) + } +} + +impl Cheatcode for coldCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { target, slot } = *self; + set_cold_slot(ccx, target, slot.into(), true); + Ok(Default::default()) + } +} + impl Cheatcode for readCallersCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; @@ -1195,3 +1238,12 @@ fn get_recorded_state_diffs(state: &mut Cheatcodes) -> BTreeMap, + /// Additional, user configurable context this Inspector has access to when inspecting a call. pub config: Arc, @@ -527,6 +533,7 @@ impl Cheatcodes { allowed_mem_writes: Default::default(), broadcast: Default::default(), broadcastable_transactions: Default::default(), + access_list: Default::default(), context: Default::default(), serialized_jsons: Default::default(), eth_deals: Default::default(), @@ -661,6 +668,11 @@ impl Cheatcodes { } } + // Apply EIP-2930 access lists. + if let Some(access_list) = &self.access_list { + ecx.env.tx.access_list = access_list.to_vec(); + } + // Apply our broadcast if let Some(broadcast) = &self.broadcast { if curr_depth >= broadcast.depth && input.caller() == broadcast.original_caller { @@ -1037,6 +1049,11 @@ where { } } + // Apply EIP-2930 access lists. + if let Some(access_list) = &self.access_list { + ecx.env.tx.access_list = access_list.to_vec(); + } + // Apply our broadcast if let Some(broadcast) = &self.broadcast { // We only apply a broadcast *to a specific depth*. diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 01c581366ffc4..9852100a7288e 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -25,9 +25,11 @@ interface Vm { 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 PotentialRevert { address reverter; bool partialMatch; bytes revertData; } + struct AccessListItem { address target; bytes32[] storageKeys; } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external; + function accessList(AccessListItem[] calldata accessList) external; function accesses(address target) external returns (bytes32[] memory readSlots, bytes32[] memory writeSlots); function activeFork() external view returns (uint256 forkId); function addr(uint256 privateKey) external pure returns (address keyAddr); @@ -166,6 +168,7 @@ interface Vm { function cloneAccount(address source, address target) external; function closeFile(string calldata path) external; function coinbase(address newCoinbase) external; + function cold(address target, bytes32 slot) external; function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) external pure returns (address); function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external pure returns (address); function computeCreateAddress(address deployer, uint256 nonce) external pure returns (address); @@ -324,6 +327,7 @@ interface Vm { function mockCalls(address callee, bytes calldata data, bytes[] calldata returnData) external; function mockCalls(address callee, uint256 msgValue, bytes calldata data, bytes[] calldata returnData) external; function mockFunction(address callee, address target, bytes calldata data) external; + function noAccessList() external; function parseAddress(string calldata stringifiedValue) external pure returns (address parsedValue); function parseBool(string calldata stringifiedValue) external pure returns (bool parsedValue); function parseBytes(string calldata stringifiedValue) external pure returns (bytes memory parsedValue); @@ -516,6 +520,7 @@ interface Vm { function tryFfi(string[] calldata commandInput) external returns (FfiResult memory result); function txGasPrice(uint256 newGasPrice) external; function unixTime() external view returns (uint256 milliseconds); + function warm(address target, bytes32 slot) external; function warp(uint256 newTimestamp) external; function writeFile(string calldata path, string calldata data) external; function writeFileBinary(string calldata path, bytes calldata data) external; diff --git a/testdata/default/cheats/AccessList.t.sol b/testdata/default/cheats/AccessList.t.sol new file mode 100644 index 0000000000000..4615ab588adba --- /dev/null +++ b/testdata/default/cheats/AccessList.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract AccessListIsolatedTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_access_list() public { + Write anotherWrite = new Write(); + Write write = new Write(); + + uint256 initial = gasleft(); + write.setNumber(1); + assertEq(initial - gasleft(), 26762); + + // set access list to anotherWrite address, hence becoming more expensive + Vm.AccessListItem[] memory accessList = new Vm.AccessListItem[](1); + bytes32[] memory readKeys = new bytes32[](0); + accessList[0] = Vm.AccessListItem(address(anotherWrite), readKeys); + vm.accessList(accessList); + + uint256 initial1 = gasleft(); + write.setNumber(2); + assertEq(initial1 - gasleft(), 29162); + + uint256 initial2 = gasleft(); + write.setNumber(3); + assertEq(initial2 - gasleft(), 29162); + + // reset access list, should take same gas as before setting + vm.noAccessList(); + uint256 initial4 = gasleft(); + write.setNumber(4); + assertEq(initial4 - gasleft(), 26762); + + uint256 initial5 = gasleft(); + write.setNumber(5); + assertEq(initial5 - gasleft(), 26762); + + vm.accessList(accessList); + uint256 initial6 = gasleft(); + write.setNumber(6); + assertEq(initial6 - gasleft(), 29162); + } +} + +contract Write { + uint256 public number = 10; + + function setNumber(uint256 _number) external { + number = _number; + } +} diff --git a/testdata/default/cheats/StorageSlotState.t.sol b/testdata/default/cheats/StorageSlotState.t.sol new file mode 100644 index 0000000000000..feb938fbd24c8 --- /dev/null +++ b/testdata/default/cheats/StorageSlotState.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract StorageSlotStateTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_gas_two_reads() public { + Read read = new Read(); + read.number(); + uint256 initial = gasleft(); + read.number(); + assertEq(initial - gasleft(), 614); + } + + function test_gas_mark_warm() public { + Read read = new Read(); + vm.warm(address(read), bytes32(0)); + uint256 initial = gasleft(); + read.number(); + assertEq(initial - gasleft(), 614); + } + + function test_gas_mark_cold() public { + Read read = new Read(); + read.number(); + vm.cold(address(read), bytes32(0)); + uint256 initial = gasleft(); + read.number(); + assertEq(initial - gasleft(), 2614); + } +} + +contract Read { + uint256 public number = 10; +}