Skip to content

Commit

Permalink
feat(cheatcodes): add vm.getStateDiff to get state diffs as string (#…
Browse files Browse the repository at this point in the history
…9435)

* feat(cheatcodes): add vm.getStateDiff() to get state diffs as string

* Nit arrow

* Add json output

* Better json format

* Rename to original and dirty

* Changes after review: split in 2 cheatcodes, rename to prev/newValues

* Slots as hex strings, add balance diffs, cleanup

* Record balance diffs only if changed. Add nonce diff placeholder

* Backoff nonce placeholder
  • Loading branch information
grandizzy authored Dec 6, 2024
1 parent 63484d0 commit 00efa0d
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

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/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ toml = { workspace = true, features = ["preserve_order"] }
tracing.workspace = true
walkdir.workspace = true
proptest.workspace = true
serde.workspace = true
40 changes: 40 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.

8 changes: 8 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@ interface Vm {
#[cheatcode(group = Evm, safety = Safe)]
function stopAndReturnStateDiff() external returns (AccountAccess[] memory accountAccesses);

/// Returns state diffs from current `vm.startStateDiffRecording` session.
#[cheatcode(group = Evm, safety = Safe)]
function getStateDiff() external view returns (string memory diff);

/// Returns state diffs from current `vm.startStateDiffRecording` session, in json format.
#[cheatcode(group = Evm, safety = Safe)]
function getStateDiffJson() external view returns (string memory diff);

// -------- Recording Map Writes --------

/// Starts recording all map SSTOREs for later retrieval.
Expand Down
143 changes: 142 additions & 1 deletion crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ use foundry_evm_core::{
use foundry_evm_traces::StackSnapshotType;
use rand::Rng;
use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY};
use std::{collections::BTreeMap, path::Path};
use std::{
collections::{btree_map::Entry, BTreeMap},
fmt::Display,
path::Path,
};

mod record_debug_step;
use record_debug_step::{convert_call_trace_to_debug_step, flatten_call_trace};
use serde::Serialize;

mod fork;
pub(crate) mod mapping;
Expand Down Expand Up @@ -76,6 +81,70 @@ pub struct DealRecord {
pub new_balance: U256,
}

/// Storage slot diff info.
#[derive(Serialize, Default)]
#[serde(rename_all = "camelCase")]
struct SlotStateDiff {
/// Initial storage value.
previous_value: B256,
/// Current storage value.
new_value: B256,
}

/// Balance diff info.
#[derive(Serialize, Default)]
#[serde(rename_all = "camelCase")]
struct BalanceDiff {
/// Initial storage value.
previous_value: U256,
/// Current storage value.
new_value: U256,
}

/// Account state diff info.
#[derive(Serialize, Default)]
#[serde(rename_all = "camelCase")]
struct AccountStateDiffs {
/// Address label, if any set.
label: Option<String>,
/// Account balance changes.
balance_diff: Option<BalanceDiff>,
/// State changes, per slot.
state_diff: BTreeMap<B256, SlotStateDiff>,
}

impl Display for AccountStateDiffs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> eyre::Result<(), std::fmt::Error> {
// Print changed account.
if let Some(label) = &self.label {
writeln!(f, "label: {label}")?;
}
// Print balance diff if changed.
if let Some(balance_diff) = &self.balance_diff {
if balance_diff.previous_value != balance_diff.new_value {
writeln!(
f,
"- balance diff: {} → {}",
balance_diff.previous_value, balance_diff.new_value
)?;
}
}
// Print state diff if any.
if !&self.state_diff.is_empty() {
writeln!(f, "- state diff:")?;
for (slot, slot_changes) in &self.state_diff {
writeln!(
f,
"@ {slot}: {} → {}",
slot_changes.previous_value, slot_changes.new_value
)?;
}
}

Ok(())
}
}

impl Cheatcode for addrCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { privateKey } = self;
Expand Down Expand Up @@ -683,6 +752,25 @@ impl Cheatcode for stopAndReturnStateDiffCall {
}
}

impl Cheatcode for getStateDiffCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let mut diffs = String::new();
let state_diffs = get_recorded_state_diffs(state);
for (address, state_diffs) in state_diffs {
diffs.push_str(&format!("{address}\n"));
diffs.push_str(&format!("{state_diffs}\n"));
}
Ok(diffs.abi_encode())
}
}

impl Cheatcode for getStateDiffJsonCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let state_diffs = get_recorded_state_diffs(state);
Ok(serde_json::to_string(&state_diffs)?.abi_encode())
}
}

impl Cheatcode for broadcastRawTransactionCall {
fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
let tx = TxEnvelope::decode(&mut self.data.as_ref())
Expand Down Expand Up @@ -1044,3 +1132,56 @@ fn genesis_account(account: &Account) -> GenesisAccount {
private_key: None,
}
}

/// Helper function to returns state diffs recorded for each changed account.
fn get_recorded_state_diffs(state: &mut Cheatcodes) -> BTreeMap<Address, AccountStateDiffs> {
let mut state_diffs: BTreeMap<Address, AccountStateDiffs> = BTreeMap::default();
if let Some(records) = &state.recorded_account_diffs_stack {
records
.iter()
.flatten()
.filter(|account_access| {
!account_access.storageAccesses.is_empty() ||
account_access.oldBalance != account_access.newBalance
})
.for_each(|account_access| {
let account_diff =
state_diffs.entry(account_access.account).or_insert(AccountStateDiffs {
label: state.labels.get(&account_access.account).cloned(),
..Default::default()
});

// Record account balance diffs.
if account_access.oldBalance != account_access.newBalance {
// Update balance diff. Do not overwrite the initial balance if already set.
if let Some(diff) = &mut account_diff.balance_diff {
diff.new_value = account_access.newBalance;
} else {
account_diff.balance_diff = Some(BalanceDiff {
previous_value: account_access.oldBalance,
new_value: account_access.newBalance,
});
}
}

// Record account state diffs.
for storage_access in &account_access.storageAccesses {
if storage_access.isWrite && !storage_access.reverted {
// Update state diff. Do not overwrite the initial value if already set.
match account_diff.state_diff.entry(storage_access.slot) {
Entry::Vacant(slot_state_diff) => {
slot_state_diff.insert(SlotStateDiff {
previous_value: storage_access.previousValue,
new_value: storage_access.newValue,
});
}
Entry::Occupied(mut slot_state_diff) => {
slot_state_diff.get_mut().new_value = storage_access.newValue;
}
}
}
}
});
}
state_diffs
}
2 changes: 2 additions & 0 deletions testdata/cheats/Vm.sol

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

37 changes: 37 additions & 0 deletions testdata/default/cheats/RecordAccountAccesses.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.18;

import "ds-test/test.sol";
import "cheats/Vm.sol";
import "../logs/console.sol";

/// @notice Helper contract with a construction that makes a call to itself then
/// optionally reverts if zero-length data is passed
Expand Down Expand Up @@ -261,6 +262,16 @@ contract RecordAccountAccessesTest is DSTest {
two.write(bytes32(uint256(5678)), bytes32(uint256(123469)));
two.write(bytes32(uint256(5678)), bytes32(uint256(1234)));

string memory diffs = cheats.getStateDiff();
assertEq(
"0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9\n- state diff:\n@ 0x00000000000000000000000000000000000000000000000000000000000004d3: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x000000000000000000000000000000000000000000000000000000000000162e\n\n0xc7183455a4C133Ae270771860664b6B7ec320bB1\n- state diff:\n@ 0x000000000000000000000000000000000000000000000000000000000000162e: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x00000000000000000000000000000000000000000000000000000000000004d2\n\n",
diffs
);
string memory diffsJson = cheats.getStateDiffJson();
assertEq(
"{\"0x5991a2df15a8f6a256d3ec51e99254cd3fb576a9\":{\"label\":null,\"balanceDiff\":null,\"stateDiff\":{\"0x00000000000000000000000000000000000000000000000000000000000004d3\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x000000000000000000000000000000000000000000000000000000000000162e\"}}},\"0xc7183455a4c133ae270771860664b6b7ec320bb1\":{\"label\":null,\"balanceDiff\":null,\"stateDiff\":{\"0x000000000000000000000000000000000000000000000000000000000000162e\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x00000000000000000000000000000000000000000000000000000000000004d2\"}}}}",
diffsJson
);
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 4, "incorrect length");

Expand Down Expand Up @@ -332,6 +343,15 @@ contract RecordAccountAccessesTest is DSTest {
// contract calls to self in constructor
SelfCaller caller = new SelfCaller{value: 2 ether}("hello2 world2");

assertEq(
"0x000000000000000000000000000000000000162e\n- balance diff: 0 \xE2\x86\x92 1000000000000000000\n\n0x1d1499e622D69689cdf9004d05Ec547d650Ff211\n- balance diff: 0 \xE2\x86\x92 2000000000000000000\n\n",
cheats.getStateDiff()
);
assertEq(
"{\"0x000000000000000000000000000000000000162e\":{\"label\":null,\"balanceDiff\":{\"previousValue\":\"0x0\",\"newValue\":\"0xde0b6b3a7640000\"},\"stateDiff\":{}},\"0x1d1499e622d69689cdf9004d05ec547d650ff211\":{\"label\":null,\"balanceDiff\":{\"previousValue\":\"0x0\",\"newValue\":\"0x1bc16d674ec80000\"},\"stateDiff\":{}}}",
cheats.getStateDiffJson()
);

Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 6);
assertEq(
Expand Down Expand Up @@ -451,6 +471,14 @@ contract RecordAccountAccessesTest is DSTest {
uint256 initBalance = address(this).balance;
cheats.startStateDiffRecording();
try this.revertingCall{value: 1 ether}(address(1234), "") {} catch {}
assertEq(
"0x00000000000000000000000000000000000004d2\n- balance diff: 0 \xE2\x86\x92 100000000000000000\n\n",
cheats.getStateDiff()
);
assertEq(
"{\"0x00000000000000000000000000000000000004d2\":{\"label\":null,\"balanceDiff\":{\"previousValue\":\"0x0\",\"newValue\":\"0x16345785d8a0000\"},\"stateDiff\":{}}}",
cheats.getStateDiffJson()
);
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 2);
assertEq(
Expand Down Expand Up @@ -768,6 +796,15 @@ contract RecordAccountAccessesTest is DSTest {
function testNestedStorage() public {
cheats.startStateDiffRecording();
nestedStorer.run();
cheats.label(address(nestedStorer), "NestedStorer");
assertEq(
"0x2e234DAe75C793f67A35089C9d99245E1C58470b\nlabel: NestedStorer\n- state diff:\n@ 0x4566fa0cd03218c55bba914d793f5e6b9113172c1f684bb5f464c08c867e8977: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x0000000000000000000000000000000000000000000000000000000000000001\n@ 0xbf57896b60daefa2c41de2feffecfc11debd98ea8c913a5170f60e53959ac00a: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x0000000000000000000000000000000000000000000000000000000000000001\n@ 0xc664893a982d78bbeab379feef216ff517b7ea73626b280723be1ace370364cd: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x0000000000000000000000000000000000000000000000000000000000000001\n@ 0xdc5330afa9872081253545dca3f448752688ff1b098b38c1abe4c4cdff4b0b0e: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x0000000000000000000000000000000000000000000000000000000000000001\n\n",
cheats.getStateDiff()
);
assertEq(
"{\"0x2e234dae75c793f67a35089c9d99245e1c58470b\":{\"label\":\"NestedStorer\",\"balanceDiff\":null,\"stateDiff\":{\"0x4566fa0cd03218c55bba914d793f5e6b9113172c1f684bb5f464c08c867e8977\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x0000000000000000000000000000000000000000000000000000000000000001\"},\"0xbf57896b60daefa2c41de2feffecfc11debd98ea8c913a5170f60e53959ac00a\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x0000000000000000000000000000000000000000000000000000000000000001\"},\"0xc664893a982d78bbeab379feef216ff517b7ea73626b280723be1ace370364cd\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x0000000000000000000000000000000000000000000000000000000000000001\"},\"0xdc5330afa9872081253545dca3f448752688ff1b098b38c1abe4c4cdff4b0b0e\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x0000000000000000000000000000000000000000000000000000000000000001\"}}}}",
cheats.getStateDiffJson()
);
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 3, "incorrect account access length");

Expand Down

0 comments on commit 00efa0d

Please sign in to comment.