Skip to content

Commit

Permalink
feat: add additional snapshot cheatcodes
Browse files Browse the repository at this point in the history
  • Loading branch information
mattsse committed Dec 8, 2023
1 parent 8a31bf1 commit 36bd3c1
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 8 deletions.
17 changes: 16 additions & 1 deletion crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,25 @@ interface Vm {

/// Revert the state of the EVM to a previous snapshot
/// Takes the snapshot ID to revert to.
/// This deletes the snapshot and all snapshots taken after the given snapshot ID.
///
/// **Note:** This does not automatically delete the snapshot. To delete the snapshot use `deleteSnapshot`.
#[cheatcode(group = Evm, safety = Unsafe)]
function revertTo(uint256 snapshotId) external returns (bool success);

/// Revert the state of the EVM to a previous snapshot and automatically deletes the snapshots
/// Takes the snapshot ID to revert to.
#[cheatcode(group = Evm, safety = Unsafe)]
function revertToAndDelete(uint256 snapshotId) external returns (bool success);

/// Removes the snapshot with the given ID created by `snapshot`.
/// Takes the snapshot ID to delete.
#[cheatcode(group = Evm, safety = Unsafe)]
function deleteSnapshot(uint256 snapshotId) external returns (bool success);

/// Removes _all_ snapshots previously created by `snapshot`.
#[cheatcode(group = Evm, safety = Unsafe)]
function deleteSnapshots() external;

// -------- Forking --------
// --- Creation and Selection ---

Expand Down
45 changes: 41 additions & 4 deletions crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use alloy_sol_types::SolValue;
use ethers_core::utils::{Genesis, GenesisAccount};
use ethers_signers::Signer;
use foundry_common::{fs::read_json_file, types::ToAlloy};
use foundry_evm_core::backend::DatabaseExt;
use foundry_evm_core::backend::{DatabaseExt, RevertSnapshotAction};
use revm::{
primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY},
EVMData,
Expand Down Expand Up @@ -327,9 +327,12 @@ impl Cheatcode for snapshotCall {
impl Cheatcode for revertToCall {
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { snapshotId } = self;
let result = if let Some(journaled_state) =
ccx.data.db.revert(*snapshotId, &ccx.data.journaled_state, ccx.data.env)
{
let result = if let Some(journaled_state) = ccx.data.db.revert(
*snapshotId,
&ccx.data.journaled_state,
ccx.data.env,
RevertSnapshotAction::RevertKeep,
) {
// we reset the evm's journaled_state to the state of the snapshot previous state
ccx.data.journaled_state = journaled_state;
true
Expand All @@ -340,6 +343,40 @@ impl Cheatcode for revertToCall {
}
}

impl Cheatcode for revertToAndDeleteCall {
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { snapshotId } = self;
let result = if let Some(journaled_state) = ccx.data.db.revert(
*snapshotId,
&ccx.data.journaled_state,
ccx.data.env,
RevertSnapshotAction::RevertRemove,
) {
// we reset the evm's journaled_state to the state of the snapshot previous state
ccx.data.journaled_state = journaled_state;
true
} else {
false
};
Ok(result.abi_encode())
}
}

impl Cheatcode for deleteSnapshotCall {
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { snapshotId } = self;
let result = ccx.data.db.delete_snapshot(*snapshotId);
Ok(result.abi_encode())
}
}
impl Cheatcode for deleteSnapshotsCall {
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self {} = self;
ccx.data.db.delete_snapshots();
Ok(Default::default())
}
}

impl Cheatcode for startStateDiffRecordingCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self {} = self;
Expand Down
26 changes: 25 additions & 1 deletion crates/evm/core/src/backend/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::{
backend::{
diagnostic::RevertDiagnostic, error::DatabaseError, Backend, DatabaseExt, LocalForkId,
RevertSnapshotAction,
},
fork::{CreateFork, ForkId},
};
Expand Down Expand Up @@ -83,6 +84,14 @@ impl<'a> FuzzBackendWrapper<'a> {
}
self.backend.to_mut()
}

/// Returns a mutable instance of the Backend if it is initialized.
fn initialized_backend_mut(&mut self) -> Option<&mut Backend> {
if self.is_initialized {
return Some(self.backend.to_mut())
}
None
}
}

impl<'a> DatabaseExt for FuzzBackendWrapper<'a> {
Expand All @@ -96,9 +105,24 @@ impl<'a> DatabaseExt for FuzzBackendWrapper<'a> {
id: U256,
journaled_state: &JournaledState,
current: &mut Env,
action: RevertSnapshotAction,
) -> Option<JournaledState> {
trace!(?id, "fuzz: revert snapshot");
self.backend_mut(current).revert(id, journaled_state, current)
self.backend_mut(current).revert(id, journaled_state, current, action)
}

fn delete_snapshot(&mut self, id: U256) -> bool {
// delete snapshot requires a previous snapshot to be initialized
if let Some(backend) = self.initialized_backend_mut() {
return backend.delete_snapshot(id)
}
false
}

fn delete_snapshots(&mut self) {
if let Some(backend) = self.initialized_backend_mut() {
backend.delete_snapshots()
}
}

fn create_fork(&mut self, fork: CreateFork) -> eyre::Result<LocalForkId> {
Expand Down
27 changes: 25 additions & 2 deletions crates/evm/core/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,26 @@ pub trait DatabaseExt: Database<Error = DatabaseError> {
/// since the snapshots was created. This way we can show logs that were emitted between
/// snapshot and its revert.
/// This will also revert any changes in the `Env` and replace it with the captured `Env` of
/// `Self::snapshot`
/// `Self::snapshot`.
///
/// Depending on [RevertSnapshotAction] it will keep the snapshot alive or delete it.
fn revert(
&mut self,
id: U256,
journaled_state: &JournaledState,
env: &mut Env,
action: RevertSnapshotAction,
) -> Option<JournaledState>;

/// Deletes the snapshot with the given `id`
///
/// Returns `true` if the snapshot was successfully deleted, `false` if no snapshot for that id
/// exists.
fn delete_snapshot(&mut self, id: U256) -> bool;

/// Deletes all snapshots.
fn delete_snapshots(&mut self);

/// Creates and also selects a new fork
///
/// This is basically `create_fork` + `select_fork`
Expand Down Expand Up @@ -918,11 +930,14 @@ impl DatabaseExt for Backend {
id: U256,
current_state: &JournaledState,
current: &mut Env,
action: RevertSnapshotAction,
) -> Option<JournaledState> {
trace!(?id, "revert snapshot");
if let Some(mut snapshot) = self.inner.snapshots.remove_at(id) {
// Re-insert snapshot to persist it
self.inner.snapshots.insert_at(snapshot.clone(), id);
if action.is_keep() {
self.inner.snapshots.insert_at(snapshot.clone(), id);
}
// need to check whether there's a global failure which means an error occurred either
// during the snapshot or even before
if self.is_global_failure(current_state) {
Expand Down Expand Up @@ -969,6 +984,14 @@ impl DatabaseExt for Backend {
}
}

fn delete_snapshot(&mut self, id: U256) -> bool {
self.inner.snapshots.remove_at(id).is_some()
}

fn delete_snapshots(&mut self) {
self.inner.snapshots.clear()
}

fn create_fork(&mut self, mut create_fork: CreateFork) -> eyre::Result<LocalForkId> {
trace!("create fork");
let (fork_id, fork, _) = self.forks.create_fork(create_fork.clone())?;
Expand Down
5 changes: 5 additions & 0 deletions crates/evm/core/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ impl<T> Snapshots<T> {
snapshot
}

/// Removes all snapshots
pub fn clear(&mut self) {
self.snapshots.clear();
}

/// Removes the snapshot with the given `id`.
///
/// Does not remove snapshots after it.
Expand Down
47 changes: 47 additions & 0 deletions testdata/cheats/Snapshots.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,53 @@ contract SnapshotTest is DSTest {
assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful");
}

function testSnapshotRevertDelete() public {
uint256 snapshot = vm.snapshot();
store.slot0 = 300;
store.slot1 = 400;

assertEq(store.slot0, 300);
assertEq(store.slot1, 400);

vm.revertToAndDelete(snapshot);
assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful");
assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful");
// nothing to revert to anymore
assert(!vm.revertTo(snapshot));
}

function testSnapshotDelete() public {
uint256 snapshot = vm.snapshot();
store.slot0 = 300;
store.slot1 = 400;

vm.deleteSnapshot(snapshot);
// nothing to revert to anymore
assert(!vm.revertTo(snapshot));
}

function testSnapshotDeleteAll() public {
uint256 snapshot = vm.snapshot();
store.slot0 = 300;
store.slot1 = 400;

vm.deleteSnapshots();
// nothing to revert to anymore
assert(!vm.revertTo(snapshot));
}

// <https://github.com/foundry-rs/foundry/issues/6411>
function testSnapshotsMany() public {
uint256 preState;
for (uint256 c = 0; c < 10; c++) {
for (uint256 cc = 0; cc < 10; cc++) {
preState = vm.snapshot();
vm.revertToAndDelete(preState);
assert(!vm.revertTo(preState));
}
}
}

// tests that snapshots can also revert changes to `block`
function testBlockValues() public {
uint256 num = block.number;
Expand Down
3 changes: 3 additions & 0 deletions testdata/cheats/Vm.sol

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

0 comments on commit 36bd3c1

Please sign in to comment.