Skip to content

Commit dc4f42f

Browse files
committed
feat(cheatcodes): add vm.getStateDiff() to get state diffs as string
1 parent fbbcc8c commit dc4f42f

File tree

5 files changed

+88
-0
lines changed

5 files changed

+88
-0
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,10 @@ interface Vm {
384384
#[cheatcode(group = Evm, safety = Safe)]
385385
function stopAndReturnStateDiff() external returns (AccountAccess[] memory accountAccesses);
386386

387+
/// Returns state diffs from current `vm.startStateDiffRecording` session.
388+
#[cheatcode(group = Evm, safety = Safe)]
389+
function getStateDiff() external view returns (string memory diff);
390+
387391
// -------- Recording Map Writes --------
388392

389393
/// Starts recording all map SSTOREs for later retrieval.

crates/cheatcodes/src/evm.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,56 @@ impl Cheatcode for stopAndReturnStateDiffCall {
683683
}
684684
}
685685

686+
/// State diffs to be displayed.
687+
/// (changed account -> (change slot -> vec of (initial value, current value) tuples))
688+
type StateDiffs = HashMap<Address, HashMap<U256, Vec<(B256, B256)>>>;
689+
690+
impl Cheatcode for getStateDiffCall {
691+
fn apply(&self, state: &mut Cheatcodes) -> Result {
692+
let Self {} = self;
693+
694+
let mut diffs = String::new();
695+
let address_label =
696+
|addr: &Address| state.labels.get(addr).cloned().unwrap_or_else(|| addr.to_string());
697+
698+
let mut changes_map: StateDiffs = HashMap::default();
699+
if let Some(records) = &state.recorded_account_diffs_stack {
700+
records
701+
.iter()
702+
.flatten()
703+
.filter(|account_access| !account_access.storageAccesses.is_empty())
704+
.for_each(|account_access| {
705+
for storage_access in &account_access.storageAccesses {
706+
if storage_access.isWrite && !storage_access.reverted {
707+
changes_map
708+
.entry(storage_access.account)
709+
.or_default()
710+
.entry(storage_access.slot.into())
711+
.or_default()
712+
.push((storage_access.previousValue, storage_access.newValue));
713+
}
714+
}
715+
});
716+
717+
for change in changes_map {
718+
// Print changed account.
719+
diffs.push_str(&format!("{}:\n", &address_label(&change.0)).to_string());
720+
for slot_changes in change.1 {
721+
// For each slot we print the initial value from first storage change recorded
722+
// and the new value from last storage change recorded.
723+
let initial_value = slot_changes.1.first().unwrap().0;
724+
let current_value = slot_changes.1.last().unwrap().1;
725+
diffs.push_str(
726+
&format!("@ {}: {} -> {}\n", slot_changes.0, initial_value, current_value)
727+
.to_string(),
728+
);
729+
}
730+
}
731+
}
732+
Ok(diffs.abi_encode())
733+
}
734+
}
735+
686736
impl Cheatcode for broadcastRawTransactionCall {
687737
fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
688738
let tx = TxEnvelope::decode(&mut self.data.as_ref())

testdata/cheats/Vm.sol

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testdata/default/cheats/RecordAccountAccesses.t.sol

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.18;
33

44
import "ds-test/test.sol";
55
import "cheats/Vm.sol";
6+
import "../logs/console.sol";
67

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

265+
string memory diffs = cheats.getStateDiff();
266+
assertEq(
267+
"0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9:\n@ 1235: 0x0000000000000000000000000000000000000000000000000000000000000000 -> 0x000000000000000000000000000000000000000000000000000000000000162e\n0xc7183455a4C133Ae270771860664b6B7ec320bB1:\n@ 5678: 0x0000000000000000000000000000000000000000000000000000000000000000 -> 0x00000000000000000000000000000000000000000000000000000000000004d2\n",
268+
diffs
269+
);
264270
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
265271
assertEq(called.length, 4, "incorrect length");
266272

@@ -332,6 +338,7 @@ contract RecordAccountAccessesTest is DSTest {
332338
// contract calls to self in constructor
333339
SelfCaller caller = new SelfCaller{value: 2 ether}("hello2 world2");
334340

341+
assertEq("", cheats.getStateDiff());
335342
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
336343
assertEq(called.length, 6);
337344
assertEq(
@@ -451,6 +458,7 @@ contract RecordAccountAccessesTest is DSTest {
451458
uint256 initBalance = address(this).balance;
452459
cheats.startStateDiffRecording();
453460
try this.revertingCall{value: 1 ether}(address(1234), "") {} catch {}
461+
assertEq("", cheats.getStateDiff());
454462
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
455463
assertEq(called.length, 2);
456464
assertEq(
@@ -768,6 +776,11 @@ contract RecordAccountAccessesTest is DSTest {
768776
function testNestedStorage() public {
769777
cheats.startStateDiffRecording();
770778
nestedStorer.run();
779+
cheats.label(address(nestedStorer), "NestedStorer");
780+
assertEq(
781+
"NestedStorer:\n@ 31391530734884398925509096751136955997235046655136458338700630915422204365175: 0x0000000000000000000000000000000000000000000000000000000000000000 -> 0x0000000000000000000000000000000000000000000000000000000000000001\n@ 86546418208203448386783321347074308435724792809315873744194221534962779865098: 0x0000000000000000000000000000000000000000000000000000000000000000 -> 0x0000000000000000000000000000000000000000000000000000000000000001\n@ 89735575844917174604881245405098157398514761457822262993733937076486162048205: 0x0000000000000000000000000000000000000000000000000000000000000000 -> 0x0000000000000000000000000000000000000000000000000000000000000001\n@ 99655811014363889343382125167956395016210879868288374279890486979400290732814: 0x0000000000000000000000000000000000000000000000000000000000000000 -> 0x0000000000000000000000000000000000000000000000000000000000000001\n",
782+
cheats.getStateDiff()
783+
);
771784
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
772785
assertEq(called.length, 3, "incorrect account access length");
773786

0 commit comments

Comments
 (0)