Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(forge): add accessList and cold/warm cheatcodes #10112

Merged
merged 4 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

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

1 change: 1 addition & 0 deletions crates/cheatcodes/spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
24 changes: 24 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 ---

Expand Down
52 changes: 52 additions & 0 deletions crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1195,3 +1238,12 @@ fn get_recorded_state_diffs(state: &mut Cheatcodes) -> BTreeMap<Address, Account
}
state_diffs
}

/// Helper function to set / unset cold storage slot of the target address.
fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) {
if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) {
if let Some(storage_slot) = account.storage.get_mut(&slot) {
storage_slot.is_cold = cold;
}
}
}
19 changes: 18 additions & 1 deletion crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ use alloy_primitives::{
map::{AddressHashMap, HashMap},
Address, Bytes, Log, TxKind, B256, U256,
};
use alloy_rpc_types::request::{TransactionInput, TransactionRequest};
use alloy_rpc_types::{
request::{TransactionInput, TransactionRequest},
AccessList,
};
use alloy_sol_types::{SolCall, SolInterface, SolValue};
use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN};
use foundry_evm_core::{
Expand Down Expand Up @@ -443,6 +446,9 @@ pub struct Cheatcodes {
/// Scripting based transactions
pub broadcastable_transactions: BroadcastableTransactions,

/// Current EIP-2930 access lists.
pub access_list: Option<AccessList>,

/// Additional, user configurable context this Inspector has access to when inspecting a call.
pub config: Arc<CheatsConfig>,

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
}
Comment on lines +671 to +674
Copy link
Member

Choose a reason for hiding this comment

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

I'm not exactly sure if this has the desired effect, because idk when the evm does respect the configured accesslist, which could be already be before execution starts?@rakita

so maybe in order to get the desired effect here we'd need to replicate how accesslists are actually handled in the evm

Copy link
Collaborator Author

@grandizzy grandizzy Mar 19, 2025

Choose a reason for hiding this comment

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

would be great to have some insights @rakita, I assumed they're applied because the test below (included in PR) shows different gas usage (access list consist of single address, no storage slot, so the difference of 2400 sums up. If adding another address there's another 2400 of gas, if adding also a storage slot, an additional 1900 is added)

        uint256 initial = gasleft();
        write.setNumber(1);
        assertEq(initial - gasleft(), 26762);

        vm.accessList(accessList);
        uint256 initial1 = gasleft();
        write.setNumber(2);
        assertEq(initial1 - gasleft(), 29162);

        // reset access list, should take same gas as before
        vm.noAccessList();
        uint256 initial4 = gasleft();
        write.setNumber(4);
        assertEq(initial4 - gasleft(), 26762);

Copy link
Member

Choose a reason for hiding this comment

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

looks like it, I think we can proceed with this as is and change in case this isn't actually the case

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, they are loaded in execution, this is okay.


// Apply our broadcast
if let Some(broadcast) = &self.broadcast {
if curr_depth >= broadcast.depth && input.caller() == broadcast.original_caller {
Expand Down Expand Up @@ -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*.
Expand Down
5 changes: 5 additions & 0 deletions testdata/cheats/Vm.sol

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

55 changes: 55 additions & 0 deletions testdata/default/cheats/AccessList.t.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading