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(cheatcodes): add vm.getStateDiff to get state diffs as string #9435

Merged
merged 15 commits into from
Dec 6, 2024
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
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
Loading