Skip to content

Commit 0a53cbe

Browse files
committed
fix(vm.cool) Persist storage changes
1 parent 855d005 commit 0a53cbe

4 files changed

Lines changed: 609 additions & 18 deletions

File tree

‎crates/cheatcodes/defs/src/vm.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ interface Vm {
255255
#[cheatcode(group = Evm, safety = Unsafe)]
256256
function store(address target, bytes32 slot, bytes32 value) external;
257257

258-
/// Marks the slots of an account and the account address as cold.
258+
/// Marks the `target` address cold, and is a no-op if the address is already cold.
259+
/// All storage slots are also made cold, but their values are preserved.
259260
#[cheatcode(group = Evm, safety = Unsafe)]
260261
function cool(address target) external;
261262

‎crates/cheatcodes/src/evm.rs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Implementations of [`Evm`](crate::Group::Evm) cheatcodes.
22
3-
use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*};
3+
use crate::{inspector::AddressState, Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*};
4+
45
use alloy_primitives::{Address, Bytes, U256};
56
use alloy_sol_types::SolValue;
67
use ethers_core::utils::{Genesis, GenesisAccount};
@@ -302,11 +303,11 @@ impl Cheatcode for storeCall {
302303

303304
impl Cheatcode for coolCall {
304305
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
305-
let Self { target } = self;
306-
if let Some(account) = ccx.data.journaled_state.state.get_mut(target) {
307-
account.unmark_touch();
308-
account.storage.clear();
309-
}
306+
let Self { target } = *self;
307+
ensure_not_precompile!(&target, ccx);
308+
// TODO: prevent or warn about cooling the to/from address in a tx
309+
ccx.state.addresses.insert(target, AddressState::Cool);
310+
ccx.state.address_storage.insert(target, HashMap::new());
310311
Ok(Default::default())
311312
}
312313
}

‎crates/cheatcodes/src/inspector.rs‎

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,179 @@ pub struct Cheatcodes {
200200
/// Breakpoints supplied by the `breakpoint` cheatcode.
201201
/// `char -> (address, pc)`
202202
pub breakpoints: Breakpoints,
203+
204+
/// Track if cool cheatcode was called on each address
205+
pub addresses: HashMap<Address, AddressState>,
206+
/// Track if an addresses storage slot is cool or not
207+
pub address_storage: HashMap<Address, HashMap<U256, StorageSlotState>>,
208+
/// How much gas to charge in the next step (op code) based on cool cheatcode calculations
209+
pub additional_gas_next_op: u64,
210+
}
211+
212+
/// Whether an Address is accessed or not
213+
#[derive(Clone, PartialEq, Debug)]
214+
pub enum AddressState {
215+
/// if already accessed, then charge WARM_STORAGE_READ_COST (100)
216+
Warm,
217+
/// charge COLD_ACCOUNT_ACCESS_COST (2600)
218+
Cool,
219+
}
220+
221+
/// Whether a Storage Slot is warm or already been modified
222+
#[derive(Clone, PartialEq, Debug)]
223+
pub enum StorageSlotState {
224+
/// charge extra based on SSTORE calculations
225+
WarmWithSLOAD,
226+
/// if SSTORE already happened, don't charge extra
227+
WarmWithSSTORE,
228+
/// same as if empty
229+
Cool,
230+
}
231+
232+
/// Function to charge extra gas per opcode based on cool cheatcode
233+
fn add_gas_from_cool_cheatcode<DB: DatabaseExt>(
234+
state: &mut Cheatcodes,
235+
interpreter: &mut Interpreter,
236+
data: &mut EVMData<'_, DB>,
237+
) -> InstructionResult {
238+
if state.addresses.len() == 0 {
239+
return InstructionResult::Continue
240+
}
241+
242+
// For gas costs, see https://eips.ethereum.org/EIPS/eip-2200, https://eips.ethereum.org/EIPS/eip-2929
243+
244+
// if previous step added gas, add it once
245+
// note that all the opcodes will already have a cost (usually 100)
246+
// so adding until it hits the gas expected for a cold key/address
247+
if state.additional_gas_next_op > 0 {
248+
interpreter.gas.record_cost(state.additional_gas_next_op);
249+
state.additional_gas_next_op = 0;
250+
}
251+
252+
// if cool cheatcode was ever called on this address
253+
let contract_address = interpreter.contract().address;
254+
255+
if state.addresses.get(&contract_address).is_some() {
256+
// check target itself
257+
match interpreter.current_opcode() {
258+
// via AccessListTracer
259+
opcode::EXTCODECOPY |
260+
opcode::EXTCODEHASH |
261+
opcode::EXTCODESIZE |
262+
opcode::BALANCE |
263+
opcode::SELFDESTRUCT => {
264+
// address is first parameter
265+
if let Ok(slot) = interpreter.stack().peek(0) {
266+
let addr: Address = Address::from_word(slot.into());
267+
268+
// COLD_ACCOUNT_ACCESS_COST is 2600
269+
// check this is done once per address, unless cheatcode is called again
270+
// ignore if same as contract address
271+
if state.addresses.get(&addr) == Some(&AddressState::Cool) {
272+
state.additional_gas_next_op = 2500;
273+
state.addresses.insert(addr, AddressState::Warm);
274+
}
275+
}
276+
}
277+
// via AccessListTracer
278+
opcode::DELEGATECALL | opcode::CALL | opcode::STATICCALL | opcode::CALLCODE => {
279+
// address is second parameter
280+
if let Ok(slot) = interpreter.stack().peek(1) {
281+
let addr: Address = Address::from_word(slot.into());
282+
283+
// COLD_ACCOUNT_ACCESS_COST is 2600
284+
// check this is done once per address, unless cheatcode is called again
285+
// ignore if same as contract address
286+
if state.addresses.get(&addr) == Some(&AddressState::Cool) {
287+
state.additional_gas_next_op = 2500;
288+
state.addresses.insert(addr, AddressState::Warm);
289+
}
290+
}
291+
}
292+
_ => {}
293+
}
294+
// check target's slots
295+
if let Some(contract_storage) = state.address_storage.get_mut(&contract_address) {
296+
match interpreter.current_opcode() {
297+
opcode::SLOAD => {
298+
let key = try_or_continue!(interpreter.stack().peek(0));
299+
300+
let account = data.journaled_state.state().get(&contract_address).unwrap();
301+
if account.storage.get(&key).is_some() {
302+
match contract_storage.get(&key) {
303+
None | Some(StorageSlotState::Cool) => {
304+
// COLD_SLOAD_COST is 2100
305+
state.additional_gas_next_op = 2000;
306+
contract_storage.insert(key, StorageSlotState::WarmWithSLOAD);
307+
}
308+
Some(_) => {}
309+
}
310+
} else {
311+
contract_storage.insert(key, StorageSlotState::WarmWithSLOAD);
312+
}
313+
}
314+
opcode::SSTORE => {
315+
let key = try_or_continue!(interpreter.stack().peek(0));
316+
let val = try_or_continue!(interpreter.stack().peek(1));
317+
318+
let account = data.journaled_state.state().get(&contract_address).unwrap();
319+
if account.storage.get(&key).is_some() {
320+
// only add gas the first time the storage is touched again
321+
match contract_storage.get(&key) {
322+
Some(StorageSlotState::WarmWithSLOAD) => {
323+
// cool keeps the slot value changes
324+
// as if the previous_or_original_value = present_value`
325+
// so include the extra gas
326+
let slot = account.storage.get(&key).unwrap();
327+
if val != slot.present_value &&
328+
slot.present_value != slot.previous_or_original_value
329+
{
330+
if slot.present_value == U256::ZERO {
331+
// SSTORE_SET_GAS is 20000
332+
state.additional_gas_next_op += 20000 - 100
333+
} else {
334+
// SSTORE_RESET_GAS is 5000 - COLD_SLOAD_COST (2100)
335+
state.additional_gas_next_op += 2900 - 100
336+
}
337+
}
338+
339+
// set slot is_warm to true
340+
contract_storage.insert(key, StorageSlotState::WarmWithSSTORE);
341+
}
342+
None | Some(StorageSlotState::Cool) => {
343+
// Means SSTORE was called without SLOAD before
344+
// COLD_SLOAD_COST is 2100
345+
state.additional_gas_next_op = 2100;
346+
347+
// cool keeps the slot value changes
348+
// as if the previous_or_original_value = present_value`
349+
// so include the extra gas
350+
let slot = account.storage.get(&key).unwrap();
351+
if val != slot.present_value &&
352+
slot.present_value != slot.previous_or_original_value
353+
{
354+
if slot.present_value == U256::ZERO {
355+
// SSTORE_SET_GAS is 20000
356+
state.additional_gas_next_op += 20000 - 100
357+
} else {
358+
// SSTORE_RESET_GAS is 5000 - COLD_SLOAD_COST (2100)
359+
state.additional_gas_next_op += 2900 - 100
360+
}
361+
}
362+
contract_storage.insert(key, StorageSlotState::WarmWithSSTORE);
363+
}
364+
Some(StorageSlotState::WarmWithSSTORE) => {}
365+
}
366+
} else {
367+
contract_storage.insert(key, StorageSlotState::WarmWithSSTORE);
368+
}
369+
}
370+
_ => {}
371+
}
372+
}
373+
}
374+
375+
InstructionResult::Continue
203376
}
204377

205378
impl Cheatcodes {
@@ -523,6 +696,16 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
523696
InstructionResult::Continue
524697
}
525698

699+
fn step_end(
700+
&mut self,
701+
interpreter: &mut Interpreter,
702+
data: &mut EVMData<'_, DB>,
703+
eval: InstructionResult,
704+
) -> InstructionResult {
705+
add_gas_from_cool_cheatcode(self, interpreter, data);
706+
eval
707+
}
708+
526709
fn log(&mut self, _: &mut EVMData<'_, DB>, address: &Address, topics: &[B256], data: &Bytes) {
527710
if !self.expected_emits.is_empty() {
528711
expect::handle_expect_emit(self, address, topics, data);

0 commit comments

Comments
 (0)