Skip to content

Commit 0dd4d31

Browse files
authored
feat(forge): add accessList and cold/warm cheatcodes (#10112)
* feat(forge): add cold/warm cheatcodes * Add accessList cheatcode * Add comment, apply default acess list only if option is some
1 parent f12f8a2 commit 0dd4d31

File tree

8 files changed

+289
-1
lines changed

8 files changed

+289
-1
lines changed

crates/cheatcodes/assets/cheatcodes.json

+96
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ impl Cheatcodes<'static> {
9090
Vm::BroadcastTxSummary::STRUCT.clone(),
9191
Vm::SignedDelegation::STRUCT.clone(),
9292
Vm::PotentialRevert::STRUCT.clone(),
93+
Vm::AccessListItem::STRUCT.clone(),
9394
]),
9495
enums: Cow::Owned(vec![
9596
Vm::CallerMode::ENUM.clone(),

crates/cheatcodes/spec/src/vm.rs

+24
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ interface Vm {
223223
bool reverted;
224224
}
225225

226+
/// An EIP-2930 access list item.
227+
struct AccessListItem {
228+
/// The address to be added in access list.
229+
address target;
230+
/// The storage keys to be added in access list.
231+
bytes32[] storageKeys;
232+
}
233+
226234
/// The result of a `stopAndReturnStateDiff` call.
227235
struct AccountAccess {
228236
/// The chain and fork the access occurred.
@@ -544,6 +552,22 @@ interface Vm {
544552
#[cheatcode(group = Evm, safety = Unsafe, status = Experimental)]
545553
function cool(address target) external;
546554

555+
/// Utility cheatcode to set an EIP-2930 access list for all subsequent transactions.
556+
#[cheatcode(group = Evm, safety = Unsafe, status = Experimental)]
557+
function accessList(AccessListItem[] calldata accessList) external;
558+
559+
/// Utility cheatcode to remove any EIP-2930 access list set by `accessList` cheatcode.
560+
#[cheatcode(group = Evm, safety = Unsafe, status = Experimental)]
561+
function noAccessList() external;
562+
563+
/// Utility cheatcode to mark specific storage slot as warm, simulating a prior read.
564+
#[cheatcode(group = Evm, safety = Unsafe, status = Experimental)]
565+
function warm(address target, bytes32 slot) external;
566+
567+
/// Utility cheatcode to mark specific storage slot as cold, simulating no prior read.
568+
#[cheatcode(group = Evm, safety = Unsafe, status = Experimental)]
569+
function cold(address target, bytes32 slot) external;
570+
547571
// -------- Call Manipulation --------
548572
// --- Mocks ---
549573

crates/cheatcodes/src/evm.rs

+52
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use foundry_evm_core::{
1616
constants::{CALLER, CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, TEST_CONTRACT_ADDRESS},
1717
};
1818
use foundry_evm_traces::StackSnapshotType;
19+
use itertools::Itertools;
1920
use rand::Rng;
2021
use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY};
2122
use std::{
@@ -585,6 +586,48 @@ impl Cheatcode for coolCall {
585586
}
586587
}
587588

589+
impl Cheatcode for accessListCall {
590+
fn apply(&self, state: &mut Cheatcodes) -> Result {
591+
let Self { accessList } = self;
592+
let access_list = accessList
593+
.iter()
594+
.map(|item| {
595+
let keys = item.storageKeys.iter().map(|key| B256::from(*key)).collect_vec();
596+
alloy_rpc_types::AccessListItem { address: item.target, storage_keys: keys }
597+
})
598+
.collect_vec();
599+
state.access_list = Some(alloy_rpc_types::AccessList::from(access_list));
600+
Ok(Default::default())
601+
}
602+
}
603+
604+
impl Cheatcode for noAccessListCall {
605+
fn apply(&self, state: &mut Cheatcodes) -> Result {
606+
let Self {} = self;
607+
// Set to empty option in order to override previous applied access list.
608+
if state.access_list.is_some() {
609+
state.access_list = Some(alloy_rpc_types::AccessList::default());
610+
}
611+
Ok(Default::default())
612+
}
613+
}
614+
615+
impl Cheatcode for warmCall {
616+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
617+
let Self { target, slot } = *self;
618+
set_cold_slot(ccx, target, slot.into(), false);
619+
Ok(Default::default())
620+
}
621+
}
622+
623+
impl Cheatcode for coldCall {
624+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
625+
let Self { target, slot } = *self;
626+
set_cold_slot(ccx, target, slot.into(), true);
627+
Ok(Default::default())
628+
}
629+
}
630+
588631
impl Cheatcode for readCallersCall {
589632
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
590633
let Self {} = self;
@@ -1195,3 +1238,12 @@ fn get_recorded_state_diffs(state: &mut Cheatcodes) -> BTreeMap<Address, Account
11951238
}
11961239
state_diffs
11971240
}
1241+
1242+
/// Helper function to set / unset cold storage slot of the target address.
1243+
fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) {
1244+
if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) {
1245+
if let Some(storage_slot) = account.storage.get_mut(&slot) {
1246+
storage_slot.is_cold = cold;
1247+
}
1248+
}
1249+
}

crates/cheatcodes/src/inspector.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ use alloy_primitives::{
2626
map::{AddressHashMap, HashMap},
2727
Address, Bytes, Log, TxKind, B256, U256,
2828
};
29-
use alloy_rpc_types::request::{TransactionInput, TransactionRequest};
29+
use alloy_rpc_types::{
30+
request::{TransactionInput, TransactionRequest},
31+
AccessList,
32+
};
3033
use alloy_sol_types::{SolCall, SolInterface, SolValue};
3134
use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN};
3235
use foundry_evm_core::{
@@ -443,6 +446,9 @@ pub struct Cheatcodes {
443446
/// Scripting based transactions
444447
pub broadcastable_transactions: BroadcastableTransactions,
445448

449+
/// Current EIP-2930 access lists.
450+
pub access_list: Option<AccessList>,
451+
446452
/// Additional, user configurable context this Inspector has access to when inspecting a call.
447453
pub config: Arc<CheatsConfig>,
448454

@@ -527,6 +533,7 @@ impl Cheatcodes {
527533
allowed_mem_writes: Default::default(),
528534
broadcast: Default::default(),
529535
broadcastable_transactions: Default::default(),
536+
access_list: Default::default(),
530537
context: Default::default(),
531538
serialized_jsons: Default::default(),
532539
eth_deals: Default::default(),
@@ -661,6 +668,11 @@ impl Cheatcodes {
661668
}
662669
}
663670

671+
// Apply EIP-2930 access lists.
672+
if let Some(access_list) = &self.access_list {
673+
ecx.env.tx.access_list = access_list.to_vec();
674+
}
675+
664676
// Apply our broadcast
665677
if let Some(broadcast) = &self.broadcast {
666678
if curr_depth >= broadcast.depth && input.caller() == broadcast.original_caller {
@@ -1037,6 +1049,11 @@ where {
10371049
}
10381050
}
10391051

1052+
// Apply EIP-2930 access lists.
1053+
if let Some(access_list) = &self.access_list {
1054+
ecx.env.tx.access_list = access_list.to_vec();
1055+
}
1056+
10401057
// Apply our broadcast
10411058
if let Some(broadcast) = &self.broadcast {
10421059
// We only apply a broadcast *to a specific depth*.

testdata/cheats/Vm.sol

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.18;
3+
4+
import "ds-test/test.sol";
5+
import "cheats/Vm.sol";
6+
7+
contract AccessListIsolatedTest is DSTest {
8+
Vm constant vm = Vm(HEVM_ADDRESS);
9+
10+
function test_access_list() public {
11+
Write anotherWrite = new Write();
12+
Write write = new Write();
13+
14+
uint256 initial = gasleft();
15+
write.setNumber(1);
16+
assertEq(initial - gasleft(), 26762);
17+
18+
// set access list to anotherWrite address, hence becoming more expensive
19+
Vm.AccessListItem[] memory accessList = new Vm.AccessListItem[](1);
20+
bytes32[] memory readKeys = new bytes32[](0);
21+
accessList[0] = Vm.AccessListItem(address(anotherWrite), readKeys);
22+
vm.accessList(accessList);
23+
24+
uint256 initial1 = gasleft();
25+
write.setNumber(2);
26+
assertEq(initial1 - gasleft(), 29162);
27+
28+
uint256 initial2 = gasleft();
29+
write.setNumber(3);
30+
assertEq(initial2 - gasleft(), 29162);
31+
32+
// reset access list, should take same gas as before setting
33+
vm.noAccessList();
34+
uint256 initial4 = gasleft();
35+
write.setNumber(4);
36+
assertEq(initial4 - gasleft(), 26762);
37+
38+
uint256 initial5 = gasleft();
39+
write.setNumber(5);
40+
assertEq(initial5 - gasleft(), 26762);
41+
42+
vm.accessList(accessList);
43+
uint256 initial6 = gasleft();
44+
write.setNumber(6);
45+
assertEq(initial6 - gasleft(), 29162);
46+
}
47+
}
48+
49+
contract Write {
50+
uint256 public number = 10;
51+
52+
function setNumber(uint256 _number) external {
53+
number = _number;
54+
}
55+
}

0 commit comments

Comments
 (0)