From 926e8d5609995817366f5e272809b469a8476d1c Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:58:48 -0700 Subject: [PATCH 01/41] feat(`forge`): sample typed storage values --- crates/common/src/contracts.rs | 27 ++- crates/evm/evm/src/executors/invariant/mod.rs | 30 ++- crates/evm/fuzz/src/invariant/mod.rs | 22 +- crates/evm/fuzz/src/strategies/state.rs | 213 ++++++++++++++++-- crates/forge/src/multi_runner.rs | 3 +- 5 files changed, 272 insertions(+), 23 deletions(-) diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 8e011609bd5a7..3693edf8f18ba 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -9,7 +9,7 @@ use foundry_compilers::{ ArtifactId, Project, ProjectCompileOutput, artifacts::{ BytecodeObject, CompactBytecode, CompactContractBytecode, CompactDeployedBytecode, - ConfigurableContractArtifact, ContractBytecodeSome, Offsets, + ConfigurableContractArtifact, ContractBytecodeSome, Offsets, StorageLayout, }, utils::canonicalized, }; @@ -75,6 +75,8 @@ pub struct ContractData { pub bytecode: Option, /// Contract runtime code. pub deployed_bytecode: Option, + /// Contract storage layout, if available. + pub storage_layout: Option, } impl ContractData { @@ -120,6 +122,29 @@ impl ContractsByArtifact { abi: abi?, bytecode: bytecode.map(Into::into), deployed_bytecode: deployed_bytecode.map(Into::into), + storage_layout: None, + }, + )) + }) + .collect(); + Self(Arc::new(map)) + } + + /// Creates a new instance from project compile output, preserving storage layouts. + pub fn with_storage_layout(output: ProjectCompileOutput) -> Self { + let map = output + .into_artifacts() + .filter_map(|(id, artifact)| { + let name = id.name.clone(); + let abi = artifact.abi?; + Some(( + id, + ContractData { + name, + abi, + bytecode: artifact.bytecode.map(Into::into), + deployed_bytecode: artifact.deployed_bytecode.map(Into::into), + storage_layout: artifact.storage_layout, }, )) }) diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 077a7c8c061ce..8ac3b2043689a 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -790,7 +790,14 @@ impl<'a> InvariantExecutor<'a> { && self.artifact_filters.matches(identifier) }) .map(|(addr, (identifier, abi))| { - (*addr, TargetedContract::new(identifier.clone(), abi.clone())) + let mut contract = TargetedContract::new(identifier.clone(), abi.clone()); + // Try to find storage layout from project contracts + if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { + &artifact.name == identifier + }) { + contract.storage_layout = contract_data.storage_layout.clone(); + } + (*addr, contract) }) .collect(); let mut contracts = TargetedContracts { inner: contracts }; @@ -833,8 +840,10 @@ impl<'a> InvariantExecutor<'a> { // Identifiers are specified as an array, so we loop through them. for identifier in artifacts { // Try to find the contract by name or identifier in the project's contracts. - if let Some(abi) = self.project_contracts.find_abi_by_name_or_identifier(identifier) - { + if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { + &artifact.name == identifier || &artifact.identifier() == identifier + }) { + let abi = &contract_data.abi; combined // Check if there's an entry for the given key in the 'combined' map. .entry(*addr) @@ -844,7 +853,11 @@ impl<'a> InvariantExecutor<'a> { entry.abi.functions.extend(abi.functions.clone()); }) // Otherwise insert it into the map. - .or_insert_with(|| TargetedContract::new(identifier.to_string(), abi)); + .or_insert_with(|| { + let mut contract = TargetedContract::new(identifier.to_string(), abi.clone()); + contract.storage_layout = contract_data.storage_layout.clone(); + contract + }); } } } @@ -944,7 +957,14 @@ impl<'a> InvariantExecutor<'a> { address ) })?; - entry.insert(TargetedContract::new(identifier.clone(), abi.clone())) + let mut contract = TargetedContract::new(identifier.clone(), abi.clone()); + // Try to find storage layout from project contracts + if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { + &artifact.name == identifier + }) { + contract.storage_layout = contract_data.storage_layout.clone(); + } + entry.insert(contract) } }; contract.add_selectors(selectors.iter().copied(), should_exclude)?; diff --git a/crates/evm/fuzz/src/invariant/mod.rs b/crates/evm/fuzz/src/invariant/mod.rs index cb419f301a95c..4066e6d5667e8 100644 --- a/crates/evm/fuzz/src/invariant/mod.rs +++ b/crates/evm/fuzz/src/invariant/mod.rs @@ -1,5 +1,6 @@ use alloy_json_abi::{Function, JsonAbi}; -use alloy_primitives::{Address, Bytes, Selector}; +use alloy_primitives::{Address, Bytes, Selector, map::HashMap}; +use foundry_compilers::artifacts::StorageLayout; use itertools::Either; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; @@ -75,6 +76,7 @@ impl FuzzRunIdentifiedContracts { abi: contract.abi.clone(), targeted_functions: functions, excluded_functions: Vec::new(), + storage_layout: contract.storage_layout.clone(), }; targets.insert(*address, contract); } @@ -146,6 +148,14 @@ impl TargetedContracts { .map(|function| format!("{}.{}", contract.identifier.clone(), function.name)) }) } + + /// Returns a map of contract addresses to their storage layouts. + pub fn get_storage_layouts(&self) -> HashMap { + self.inner + .iter() + .filter_map(|(addr, c)| c.storage_layout.as_ref().map(|layout| (*addr, layout.clone()))) + .collect() + } } impl std::ops::Deref for TargetedContracts { @@ -173,12 +183,20 @@ pub struct TargetedContract { pub targeted_functions: Vec, /// The excluded functions of the contract. pub excluded_functions: Vec, + /// The contract's storage layout, if available. + pub storage_layout: Option, } impl TargetedContract { /// Returns a new `TargetedContract` instance. pub fn new(identifier: String, abi: JsonAbi) -> Self { - Self { identifier, abi, targeted_functions: Vec::new(), excluded_functions: Vec::new() } + Self { + identifier, + abi, + targeted_functions: Vec::new(), + excluded_functions: Vec::new(), + storage_layout: None, + } } /// Helper to retrieve functions to fuzz for specified abi. diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index bb166e53177f9..22d026284aac7 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -6,6 +6,7 @@ use alloy_primitives::{ map::{AddressIndexSet, B256IndexSet, HashMap}, }; use foundry_common::ignore_metadata_hash; +use foundry_compilers::artifacts::StorageLayout; use foundry_config::FuzzDictionaryConfig; use foundry_evm_core::utils::StateChangeset; use parking_lot::{RawRwLock, RwLock, lock_api::RwLockReadGuard}; @@ -72,8 +73,10 @@ impl EvmFuzzState { let (target_abi, target_function) = targets.fuzzed_artifacts(tx); dict.insert_logs_values(target_abi, logs, run_depth); dict.insert_result_values(target_function, result, run_depth); + // Get storage layouts for contracts in the state changeset + let storage_layouts = targets.get_storage_layouts(); + dict.insert_new_state_values(state_changeset, &storage_layouts); } - dict.insert_new_state_values(state_changeset); } /// Removes all newly added entries from the dictionary. @@ -151,7 +154,7 @@ impl FuzzDictionary { // Sort storage values before inserting to ensure deterministic dictionary. let values = account.storage.iter().collect::>(); for (slot, value) in values { - self.insert_storage_value(slot, value); + self.insert_storage_value(slot, value, None); } } } @@ -226,7 +229,11 @@ impl FuzzDictionary { /// Insert values from call state changeset into fuzz dictionary. /// These values are removed at the end of current run. - fn insert_new_state_values(&mut self, state_changeset: &StateChangeset) { + fn insert_new_state_values( + &mut self, + state_changeset: &StateChangeset, + storage_layouts: &HashMap, + ) { for (address, account) in state_changeset { // Insert basic account information. self.insert_value(address.into_word()); @@ -234,8 +241,9 @@ impl FuzzDictionary { self.insert_push_bytes_values(address, &account.info); // Insert storage values. if self.config.include_storage { + let storage_layout = storage_layouts.get(address); for (slot, value) in &account.storage { - self.insert_storage_value(slot, &value.present_value); + self.insert_storage_value(slot, &value.present_value, storage_layout); } } } @@ -290,17 +298,86 @@ impl FuzzDictionary { /// Insert values from single storage slot and storage value into fuzz dictionary. /// If storage values are newly collected then they are removed at the end of current run. - fn insert_storage_value(&mut self, storage_slot: &U256, storage_value: &U256) { + fn insert_storage_value( + &mut self, + storage_slot: &U256, + storage_value: &U256, + storage_layout: Option<&StorageLayout>, + ) { + // Always insert the slot itself self.insert_value(B256::from(*storage_slot)); - self.insert_value(B256::from(*storage_value)); - // also add the value below and above the storage value to the dictionary. - if *storage_value != U256::ZERO { - let below_value = storage_value - U256::from(1); - self.insert_value(B256::from(below_value)); - } - if *storage_value != U256::MAX { - let above_value = storage_value + U256::from(1); - self.insert_value(B256::from(above_value)); + + // Try to determine the type of this storage slot + let storage_type = storage_layout.and_then(|layout| { + // Convert slot to string for comparison + let slot_str = format!("{:#x}", storage_slot); + + // Find the storage entry for this slot + layout + .storage + .iter() + .find(|s| s.slot == slot_str || s.slot == storage_slot.to_string()) + .and_then(|storage| { + // Look up the type information + layout + .types + .get(&storage.storage_type) + .map(|t| DynSolType::parse(&t.label).ok()) + .flatten() + }) + }); + + // If we have type information, only insert as a sample value with the correct type + if let Some(sol_type) = storage_type { + // Only insert values for types that can be represented as a single word + match &sol_type { + DynSolType::Address + | DynSolType::Uint(_) + | DynSolType::Int(_) + | DynSolType::Bool + | DynSolType::FixedBytes(_) + | DynSolType::Bytes => { + // Insert as a typed sample value + self.sample_values + .entry(sol_type.clone()) + .or_default() + .insert(B256::from(*storage_value)); + + // For numeric types, also add adjacent values as samples + if matches!(sol_type, DynSolType::Uint(_) | DynSolType::Int(_)) { + if *storage_value != U256::ZERO { + let below_value = storage_value - U256::from(1); + self.sample_values + .entry(sol_type.clone()) + .or_default() + .insert(B256::from(below_value)); + } + if *storage_value != U256::MAX { + let above_value = storage_value + U256::from(1); + self.sample_values + .entry(sol_type.clone()) + .or_default() + .insert(B256::from(above_value)); + } + } + } + _ => { + // For complex types (arrays, mappings, structs), insert as raw value + self.insert_value(B256::from(*storage_value)); + } + } + } else { + // No type information available, insert as raw values (old behavior) + self.insert_value(B256::from(*storage_value)); + // also add the value below and above the storage value to the dictionary. + if *storage_value != U256::ZERO { + let below_value = storage_value - U256::from(1); + self.insert_value(B256::from(below_value)); + } + if *storage_value != U256::MAX { + let above_value = storage_value + U256::from(1); + self.insert_value(B256::from(above_value)); + } } } @@ -385,3 +462,111 @@ impl FuzzDictionary { ); } } + +#[cfg(test)] +mod tests { + use super::*; + use foundry_compilers::artifacts::{Storage, StorageType}; + + #[test] + fn test_type_aware_storage_insertion() { + // Create a simple storage layout for testing + let mut storage_layout = StorageLayout { + storage: vec![ + Storage { + ast_id: 1, + contract: "TestContract".to_string(), + label: "myUint".to_string(), + offset: 0, + slot: "0x0".to_string(), + storage_type: "t_uint256".to_string(), + }, + Storage { + ast_id: 2, + contract: "TestContract".to_string(), + label: "myAddress".to_string(), + offset: 0, + slot: "0x1".to_string(), + storage_type: "t_address".to_string(), + }, + ], + types: BTreeMap::new(), + }; + + // Add type information + storage_layout.types.insert( + "t_uint256".to_string(), + StorageType { + encoding: "inplace".to_string(), + key: None, + label: "uint256".to_string(), + number_of_bytes: "32".to_string(), + value: None, + other: BTreeMap::new(), + }, + ); + storage_layout.types.insert( + "t_address".to_string(), + StorageType { + encoding: "inplace".to_string(), + key: None, + label: "address".to_string(), + number_of_bytes: "20".to_string(), + value: None, + other: BTreeMap::new(), + }, + ); + + // Create a fuzz dictionary + let config = FuzzDictionaryConfig::default(); + let mut dict = FuzzDictionary::new(config); + + // Test inserting a uint256 value + let uint_slot = U256::from(0); + let uint_value = U256::from(42); + dict.insert_storage_value(&uint_slot, &uint_value, Some(&storage_layout)); + + // Test inserting an address value + let addr_slot = U256::from(1); + let addr_value = U256::from_be_bytes([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 12 zeros + 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, + 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, + ]); + dict.insert_storage_value(&addr_slot, &addr_value, Some(&storage_layout)); + + // Check that values were inserted as typed samples + let uint_type = DynSolType::Uint(256); + let addr_type = DynSolType::Address; + + assert!(dict.samples(&uint_type).is_some()); + assert!(dict.samples(&addr_type).is_some()); + + // Check that the uint value was inserted with adjacent values + let uint_samples = dict.samples(&uint_type).unwrap(); + assert!(uint_samples.contains(&B256::from(uint_value))); + assert!(uint_samples.contains(&B256::from(uint_value - U256::from(1)))); + assert!(uint_samples.contains(&B256::from(uint_value + U256::from(1)))); + + // Check that the address value was inserted + let addr_samples = dict.samples(&addr_type).unwrap(); + assert!(addr_samples.contains(&B256::from(addr_value))); + } + + #[test] + fn test_storage_insertion_without_layout() { + let config = FuzzDictionaryConfig::default(); + let mut dict = FuzzDictionary::new(config); + + // Test inserting without storage layout (fallback behavior) + let slot = U256::from(5); + let value = U256::from(100); + dict.insert_storage_value(&slot, &value, None); + + // Without storage layout, values should be inserted as raw values + assert!(dict.values().contains(&B256::from(slot))); + assert!(dict.values().contains(&B256::from(value))); + assert!(dict.values().contains(&B256::from(value - U256::from(1)))); + assert!(dict.values().contains(&B256::from(value + U256::from(1)))); + } +} diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 76ae5d6da10e9..184840f3b9833 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -519,7 +519,8 @@ impl MultiContractRunnerBuilder { } } - let known_contracts = ContractsByArtifact::new(linked_contracts); + // Create known contracts with storage layout information + let known_contracts = ContractsByArtifact::with_storage_layout(output.clone()); Ok(MultiContractRunner { contracts: deployable_contracts, From 8ec91b34f977727bca1285a4328a2508a761af5e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:07:17 -0700 Subject: [PATCH 02/41] arc it --- crates/common/src/contracts.rs | 4 ++-- crates/evm/evm/src/executors/invariant/mod.rs | 6 +++--- crates/evm/fuzz/src/invariant/mod.rs | 8 ++++---- crates/evm/fuzz/src/strategies/state.rs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 3693edf8f18ba..dda35cffeace4 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -76,7 +76,7 @@ pub struct ContractData { /// Contract runtime code. pub deployed_bytecode: Option, /// Contract storage layout, if available. - pub storage_layout: Option, + pub storage_layout: Option>, } impl ContractData { @@ -144,7 +144,7 @@ impl ContractsByArtifact { abi, bytecode: artifact.bytecode.map(Into::into), deployed_bytecode: artifact.deployed_bytecode.map(Into::into), - storage_layout: artifact.storage_layout, + storage_layout: artifact.storage_layout.map(Arc::new), }, )) }) diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 8ac3b2043689a..b6a0bd045bde0 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -795,7 +795,7 @@ impl<'a> InvariantExecutor<'a> { if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { &artifact.name == identifier }) { - contract.storage_layout = contract_data.storage_layout.clone(); + contract.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone); } (*addr, contract) }) @@ -855,7 +855,7 @@ impl<'a> InvariantExecutor<'a> { // Otherwise insert it into the map. .or_insert_with(|| { let mut contract = TargetedContract::new(identifier.to_string(), abi.clone()); - contract.storage_layout = contract_data.storage_layout.clone(); + contract.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone); contract }); } @@ -962,7 +962,7 @@ impl<'a> InvariantExecutor<'a> { if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { &artifact.name == identifier }) { - contract.storage_layout = contract_data.storage_layout.clone(); + contract.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone); } entry.insert(contract) } diff --git a/crates/evm/fuzz/src/invariant/mod.rs b/crates/evm/fuzz/src/invariant/mod.rs index 4066e6d5667e8..6bc7506bd0b8a 100644 --- a/crates/evm/fuzz/src/invariant/mod.rs +++ b/crates/evm/fuzz/src/invariant/mod.rs @@ -76,7 +76,7 @@ impl FuzzRunIdentifiedContracts { abi: contract.abi.clone(), targeted_functions: functions, excluded_functions: Vec::new(), - storage_layout: contract.storage_layout.clone(), + storage_layout: contract.storage_layout.as_ref().map(Arc::clone), }; targets.insert(*address, contract); } @@ -150,10 +150,10 @@ impl TargetedContracts { } /// Returns a map of contract addresses to their storage layouts. - pub fn get_storage_layouts(&self) -> HashMap { + pub fn get_storage_layouts(&self) -> HashMap> { self.inner .iter() - .filter_map(|(addr, c)| c.storage_layout.as_ref().map(|layout| (*addr, layout.clone()))) + .filter_map(|(addr, c)| c.storage_layout.as_ref().map(|layout| (*addr, Arc::clone(layout)))) .collect() } } @@ -184,7 +184,7 @@ pub struct TargetedContract { /// The excluded functions of the contract. pub excluded_functions: Vec, /// The contract's storage layout, if available. - pub storage_layout: Option, + pub storage_layout: Option>, } impl TargetedContract { diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 22d026284aac7..8c24194f70470 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -232,7 +232,7 @@ impl FuzzDictionary { fn insert_new_state_values( &mut self, state_changeset: &StateChangeset, - storage_layouts: &HashMap, + storage_layouts: &HashMap>, ) { for (address, account) in state_changeset { // Insert basic account information. @@ -241,7 +241,7 @@ impl FuzzDictionary { self.insert_push_bytes_values(address, &account.info); // Insert storage values. if self.config.include_storage { - let storage_layout = storage_layouts.get(address); + let storage_layout = storage_layouts.get(address).map(|arc| arc.as_ref()); for (slot, value) in &account.storage { self.insert_storage_value(slot, &value.present_value, storage_layout); } From cfd4a15760ffab4fee369e18d02ee118c16d25ca Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:07:31 -0700 Subject: [PATCH 03/41] nit --- crates/evm/fuzz/src/strategies/state.rs | 108 ------------------------ 1 file changed, 108 deletions(-) diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 8c24194f70470..83f1b234c2d13 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -462,111 +462,3 @@ impl FuzzDictionary { ); } } - -#[cfg(test)] -mod tests { - use super::*; - use foundry_compilers::artifacts::{Storage, StorageType}; - - #[test] - fn test_type_aware_storage_insertion() { - // Create a simple storage layout for testing - let mut storage_layout = StorageLayout { - storage: vec![ - Storage { - ast_id: 1, - contract: "TestContract".to_string(), - label: "myUint".to_string(), - offset: 0, - slot: "0x0".to_string(), - storage_type: "t_uint256".to_string(), - }, - Storage { - ast_id: 2, - contract: "TestContract".to_string(), - label: "myAddress".to_string(), - offset: 0, - slot: "0x1".to_string(), - storage_type: "t_address".to_string(), - }, - ], - types: BTreeMap::new(), - }; - - // Add type information - storage_layout.types.insert( - "t_uint256".to_string(), - StorageType { - encoding: "inplace".to_string(), - key: None, - label: "uint256".to_string(), - number_of_bytes: "32".to_string(), - value: None, - other: BTreeMap::new(), - }, - ); - storage_layout.types.insert( - "t_address".to_string(), - StorageType { - encoding: "inplace".to_string(), - key: None, - label: "address".to_string(), - number_of_bytes: "20".to_string(), - value: None, - other: BTreeMap::new(), - }, - ); - - // Create a fuzz dictionary - let config = FuzzDictionaryConfig::default(); - let mut dict = FuzzDictionary::new(config); - - // Test inserting a uint256 value - let uint_slot = U256::from(0); - let uint_value = U256::from(42); - dict.insert_storage_value(&uint_slot, &uint_value, Some(&storage_layout)); - - // Test inserting an address value - let addr_slot = U256::from(1); - let addr_value = U256::from_be_bytes([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 12 zeros - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, - 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, - ]); - dict.insert_storage_value(&addr_slot, &addr_value, Some(&storage_layout)); - - // Check that values were inserted as typed samples - let uint_type = DynSolType::Uint(256); - let addr_type = DynSolType::Address; - - assert!(dict.samples(&uint_type).is_some()); - assert!(dict.samples(&addr_type).is_some()); - - // Check that the uint value was inserted with adjacent values - let uint_samples = dict.samples(&uint_type).unwrap(); - assert!(uint_samples.contains(&B256::from(uint_value))); - assert!(uint_samples.contains(&B256::from(uint_value - U256::from(1)))); - assert!(uint_samples.contains(&B256::from(uint_value + U256::from(1)))); - - // Check that the address value was inserted - let addr_samples = dict.samples(&addr_type).unwrap(); - assert!(addr_samples.contains(&B256::from(addr_value))); - } - - #[test] - fn test_storage_insertion_without_layout() { - let config = FuzzDictionaryConfig::default(); - let mut dict = FuzzDictionary::new(config); - - // Test inserting without storage layout (fallback behavior) - let slot = U256::from(5); - let value = U256::from(100); - dict.insert_storage_value(&slot, &value, None); - - // Without storage layout, values should be inserted as raw values - assert!(dict.values().contains(&B256::from(slot))); - assert!(dict.values().contains(&B256::from(value))); - assert!(dict.values().contains(&B256::from(value - U256::from(1)))); - assert!(dict.values().contains(&B256::from(value + U256::from(1)))); - } -} From c56c920cca5378834185b46df597dea653c6b628 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:11:18 -0700 Subject: [PATCH 04/41] clippy --- crates/evm/evm/src/executors/invariant/mod.rs | 26 +++++++++++-------- crates/evm/fuzz/src/invariant/mod.rs | 4 ++- crates/evm/fuzz/src/strategies/state.rs | 5 ++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index b6a0bd045bde0..ba09e374b1d39 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -792,9 +792,9 @@ impl<'a> InvariantExecutor<'a> { .map(|(addr, (identifier, abi))| { let mut contract = TargetedContract::new(identifier.clone(), abi.clone()); // Try to find storage layout from project contracts - if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { - &artifact.name == identifier - }) { + if let Some((_, contract_data)) = + self.project_contracts.iter().find(|(artifact, _)| &artifact.name == identifier) + { contract.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone); } (*addr, contract) @@ -840,9 +840,11 @@ impl<'a> InvariantExecutor<'a> { // Identifiers are specified as an array, so we loop through them. for identifier in artifacts { // Try to find the contract by name or identifier in the project's contracts. - if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { - &artifact.name == identifier || &artifact.identifier() == identifier - }) { + if let Some((_, contract_data)) = + self.project_contracts.iter().find(|(artifact, _)| { + &artifact.name == identifier || &artifact.identifier() == identifier + }) + { let abi = &contract_data.abi; combined // Check if there's an entry for the given key in the 'combined' map. @@ -854,8 +856,10 @@ impl<'a> InvariantExecutor<'a> { }) // Otherwise insert it into the map. .or_insert_with(|| { - let mut contract = TargetedContract::new(identifier.to_string(), abi.clone()); - contract.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone); + let mut contract = + TargetedContract::new(identifier.to_string(), abi.clone()); + contract.storage_layout = + contract_data.storage_layout.as_ref().map(Arc::clone); contract }); } @@ -959,9 +963,9 @@ impl<'a> InvariantExecutor<'a> { })?; let mut contract = TargetedContract::new(identifier.clone(), abi.clone()); // Try to find storage layout from project contracts - if let Some((_, contract_data)) = self.project_contracts.iter().find(|(artifact, _)| { - &artifact.name == identifier - }) { + if let Some((_, contract_data)) = + self.project_contracts.iter().find(|(artifact, _)| &artifact.name == identifier) + { contract.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone); } entry.insert(contract) diff --git a/crates/evm/fuzz/src/invariant/mod.rs b/crates/evm/fuzz/src/invariant/mod.rs index 6bc7506bd0b8a..bac4a4a0c0794 100644 --- a/crates/evm/fuzz/src/invariant/mod.rs +++ b/crates/evm/fuzz/src/invariant/mod.rs @@ -153,7 +153,9 @@ impl TargetedContracts { pub fn get_storage_layouts(&self) -> HashMap> { self.inner .iter() - .filter_map(|(addr, c)| c.storage_layout.as_ref().map(|layout| (*addr, Arc::clone(layout)))) + .filter_map(|(addr, c)| { + c.storage_layout.as_ref().map(|layout| (*addr, Arc::clone(layout))) + }) .collect() } } diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 83f1b234c2d13..b6e578d6c5940 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -310,7 +310,7 @@ impl FuzzDictionary { // Try to determine the type of this storage slot let storage_type = storage_layout.and_then(|layout| { // Convert slot to string for comparison - let slot_str = format!("{:#x}", storage_slot); + let slot_str = format!("{storage_slot:#x}"); // Find the storage entry for this slot layout @@ -322,8 +322,7 @@ impl FuzzDictionary { layout .types .get(&storage.storage_type) - .map(|t| DynSolType::parse(&t.label).ok()) - .flatten() + .and_then(|t| DynSolType::parse(&t.label).ok()) }) }); From 90a1dea622c8bee52d0cdb7fa650bc64f7005a35 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:13:25 -0700 Subject: [PATCH 05/41] nit --- crates/evm/fuzz/src/strategies/state.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index b6e578d6c5940..bf75d5e39f6cf 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -309,21 +309,14 @@ impl FuzzDictionary { // Try to determine the type of this storage slot let storage_type = storage_layout.and_then(|layout| { - // Convert slot to string for comparison - let slot_str = format!("{storage_slot:#x}"); - // Find the storage entry for this slot - layout - .storage - .iter() - .find(|s| s.slot == slot_str || s.slot == storage_slot.to_string()) - .and_then(|storage| { - // Look up the type information - layout - .types - .get(&storage.storage_type) - .and_then(|t| DynSolType::parse(&t.label).ok()) - }) + layout.storage.iter().find(|s| s.slot == storage_slot.to_string()).and_then(|storage| { + // Look up the type information + layout + .types + .get(&storage.storage_type) + .and_then(|t| DynSolType::parse(&t.label).ok()) + }) }); // If we have type information, only insert as a sample value with the correct type From e7caa867f9d1c3906ee047fe86335fd9e81ce569 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:57:00 -0700 Subject: [PATCH 06/41] strip file prefixes --- crates/forge/src/multi_runner.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 184840f3b9833..a6ec3caa2a9a2 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -520,7 +520,9 @@ impl MultiContractRunnerBuilder { } // Create known contracts with storage layout information - let known_contracts = ContractsByArtifact::with_storage_layout(output.clone()); + let known_contracts = ContractsByArtifact::with_storage_layout( + output.clone().with_stripped_file_prefixes(root) + ); Ok(MultiContractRunner { contracts: deployable_contracts, From bccf3adbda2f248347b4445c385d860f3291e90d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:58:47 -0700 Subject: [PATCH 07/41] fmt --- crates/forge/src/multi_runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index a6ec3caa2a9a2..590e95f1e23c2 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -521,7 +521,7 @@ impl MultiContractRunnerBuilder { // Create known contracts with storage layout information let known_contracts = ContractsByArtifact::with_storage_layout( - output.clone().with_stripped_file_prefixes(root) + output.clone().with_stripped_file_prefixes(root), ); Ok(MultiContractRunner { From fd6ecd6b5c9f2b9f03a163637953aa96c2915394 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:34:53 -0700 Subject: [PATCH 08/41] don't add adjacent values to sample --- crates/evm/fuzz/src/strategies/state.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index bf75d5e39f6cf..161aaa6e12587 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -334,24 +334,6 @@ impl FuzzDictionary { .entry(sol_type.clone()) .or_default() .insert(B256::from(*storage_value)); - - // For numeric types, also add adjacent values as samples - if matches!(sol_type, DynSolType::Uint(_) | DynSolType::Int(_)) { - if *storage_value != U256::ZERO { - let below_value = storage_value - U256::from(1); - self.sample_values - .entry(sol_type.clone()) - .or_default() - .insert(B256::from(below_value)); - } - if *storage_value != U256::MAX { - let above_value = storage_value + U256::from(1); - self.sample_values - .entry(sol_type.clone()) - .or_default() - .insert(B256::from(above_value)); - } - } } _ => { // For complex types (arrays, mappings, structs), insert as raw value From f62bcb82e0ccbe71c8cffcf4107450488f41e77d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:17:49 -0700 Subject: [PATCH 09/41] feat(cheatcodes): add contract identifier to AccountStateDiffs --- crates/cheatcodes/src/evm.rs | 80 ++++++++++++++++--- .../cheats/RecordAccountAccesses.t.sol | 11 +-- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index e225a20cd9b2a..d090ce53f5786 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -27,7 +27,7 @@ use revm::{ state::Account, }; use std::{ - collections::{BTreeMap, btree_map::Entry}, + collections::{BTreeMap, HashSet, btree_map::Entry}, fmt::Display, path::Path, }; @@ -121,6 +121,8 @@ struct BalanceDiff { struct AccountStateDiffs { /// Address label, if any set. label: Option, + /// Contract name from artifact, if found. + contract: Option, /// Account balance changes. balance_diff: Option, /// State changes, per slot. @@ -133,6 +135,9 @@ impl Display for AccountStateDiffs { if let Some(label) = &self.label { writeln!(f, "label: {label}")?; } + if let Some(contract) = &self.contract { + writeln!(f, "contract: {contract}")?; + } // Print balance diff if changed. if let Some(balance_diff) = &self.balance_diff && balance_diff.previous_value != balance_diff.new_value @@ -814,9 +819,9 @@ impl Cheatcode for stopAndReturnStateDiffCall { } impl Cheatcode for getStateDiffCall { - fn apply(&self, state: &mut Cheatcodes) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let mut diffs = String::new(); - let state_diffs = get_recorded_state_diffs(state); + let state_diffs = get_recorded_state_diffs(ccx); for (address, state_diffs) in state_diffs { diffs.push_str(&format!("{address}\n")); diffs.push_str(&format!("{state_diffs}\n")); @@ -826,8 +831,8 @@ impl Cheatcode for getStateDiffCall { } impl Cheatcode for getStateDiffJsonCall { - fn apply(&self, state: &mut Cheatcodes) -> Result { - let state_diffs = get_recorded_state_diffs(state); + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let state_diffs = get_recorded_state_diffs(ccx); Ok(serde_json::to_string(&state_diffs)?.abi_encode()) } } @@ -1200,9 +1205,36 @@ fn genesis_account(account: &Account) -> GenesisAccount { } /// Helper function to returns state diffs recorded for each changed account. -fn get_recorded_state_diffs(state: &mut Cheatcodes) -> BTreeMap { +fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap { let mut state_diffs: BTreeMap = BTreeMap::default(); - if let Some(records) = &state.recorded_account_diffs_stack { + + // First, collect all unique addresses we need to look up + let mut addresses_to_lookup = HashSet::new(); + if let Some(records) = &ccx.state.recorded_account_diffs_stack { + for account_access in records.iter().flatten() { + if !account_access.storageAccesses.is_empty() + || account_access.oldBalance != account_access.newBalance + { + addresses_to_lookup.insert(account_access.account); + for storage_access in &account_access.storageAccesses { + if storage_access.isWrite && !storage_access.reverted { + addresses_to_lookup.insert(storage_access.account); + } + } + } + } + } + + // Look up contract names for all addresses + let mut contract_names = HashMap::new(); + for address in addresses_to_lookup { + if let Some(name) = get_contract_name(ccx, address) { + contract_names.insert(address, name); + } + } + + // Now process the records + if let Some(records) = &ccx.state.recorded_account_diffs_stack { records .iter() .flatten() @@ -1216,7 +1248,8 @@ fn get_recorded_state_diffs(state: &mut Cheatcodes) -> BTreeMap BTreeMap BTreeMap Option { + // Check if we have available artifacts to match against + let artifacts = ccx.state.config.available_artifacts.as_ref()?; + + // Try to load the account and get its code + let account = ccx.ecx.journaled_state.load_account(address).ok()?; + let code = account.info.code.as_ref()?; + + // Skip if code is empty + if code.is_empty() { + return None; + } + + // Try to find the artifact by deployed code + let code_bytes = code.original_bytes(); + if let Some((artifact_id, _)) = artifacts.find_by_deployed_code_exact(&code_bytes) { + return Some(artifact_id.identifier()); + } + + // Fallback to fuzzy matching if exact match fails + if let Some((artifact_id, _)) = artifacts.find_by_deployed_code(&code_bytes) { + return Some(artifact_id.identifier()); + } + + None +} + /// Helper function to set / unset cold storage slot of the target address. fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) { if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) diff --git a/testdata/default/cheats/RecordAccountAccesses.t.sol b/testdata/default/cheats/RecordAccountAccesses.t.sol index 63100f51f1284..df69828516d6d 100644 --- a/testdata/default/cheats/RecordAccountAccesses.t.sol +++ b/testdata/default/cheats/RecordAccountAccesses.t.sol @@ -264,12 +264,12 @@ contract RecordAccountAccessesTest is DSTest { 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", + "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9\ncontract: default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\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\"}}}}", + "{\"0x5991a2df15a8f6a256d3ec51e99254cd3fb576a9\":{\"label\":null,\"contract\":\"default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\",\"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()); @@ -347,6 +347,7 @@ contract RecordAccountAccessesTest is DSTest { string memory expectedStateDiff = "0x000000000000000000000000000000000000162e\n- balance diff: 0 \xE2\x86\x92 1000000000000000000\n\n"; expectedStateDiff = string.concat(expectedStateDiff, callerAddress); + expectedStateDiff = string.concat(expectedStateDiff, "\ncontract: default/cheats/RecordAccountAccesses.t.sol:SelfCaller"); expectedStateDiff = string.concat(expectedStateDiff, "\n- balance diff: 0 \xE2\x86\x92 2000000000000000000\n\n"); assertEq(expectedStateDiff, cheats.getStateDiff()); @@ -474,7 +475,7 @@ contract RecordAccountAccessesTest is DSTest { cheats.getStateDiff() ); assertEq( - "{\"0x00000000000000000000000000000000000004d2\":{\"label\":null,\"balanceDiff\":{\"previousValue\":\"0x0\",\"newValue\":\"0x16345785d8a0000\"},\"stateDiff\":{}}}", + "{\"0x00000000000000000000000000000000000004d2\":{\"label\":null,\"contract\":null,\"balanceDiff\":{\"previousValue\":\"0x0\",\"newValue\":\"0x16345785d8a0000\"},\"stateDiff\":{}}}", cheats.getStateDiffJson() ); Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff()); @@ -796,11 +797,11 @@ contract RecordAccountAccessesTest is DSTest { 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", + "0x2e234DAe75C793f67A35089C9d99245E1C58470b\nlabel: NestedStorer\ncontract: default/cheats/RecordAccountAccesses.t.sol: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\"}}}}", + "{\"0x2e234dae75c793f67a35089c9d99245e1c58470b\":{\"label\":\"NestedStorer\",\"contract\":\"default/cheats/RecordAccountAccesses.t.sol: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()); From 1b4520fff3ccad006717a9129ece7ec5af00398a Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:22:04 -0700 Subject: [PATCH 10/41] forge fmt --- testdata/default/cheats/RecordAccountAccesses.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testdata/default/cheats/RecordAccountAccesses.t.sol b/testdata/default/cheats/RecordAccountAccesses.t.sol index df69828516d6d..02089b5335358 100644 --- a/testdata/default/cheats/RecordAccountAccesses.t.sol +++ b/testdata/default/cheats/RecordAccountAccesses.t.sol @@ -347,7 +347,8 @@ contract RecordAccountAccessesTest is DSTest { string memory expectedStateDiff = "0x000000000000000000000000000000000000162e\n- balance diff: 0 \xE2\x86\x92 1000000000000000000\n\n"; expectedStateDiff = string.concat(expectedStateDiff, callerAddress); - expectedStateDiff = string.concat(expectedStateDiff, "\ncontract: default/cheats/RecordAccountAccesses.t.sol:SelfCaller"); + expectedStateDiff = + string.concat(expectedStateDiff, "\ncontract: default/cheats/RecordAccountAccesses.t.sol:SelfCaller"); expectedStateDiff = string.concat(expectedStateDiff, "\n- balance diff: 0 \xE2\x86\x92 2000000000000000000\n\n"); assertEq(expectedStateDiff, cheats.getStateDiff()); From 729ea8ea659760c3b0b89b468015ced0e94ca8e2 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:03:46 +0530 Subject: [PATCH 11/41] doc nits --- crates/evm/fuzz/src/strategies/state.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 50c269952ef10..6a61775bc1bec 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -401,6 +401,8 @@ impl FuzzDictionary { /// ``` /// /// A storage value is inserted if and only if it can be decoded into one of the mapping + /// [`DynSolType`] value types found in the [`StorageLayout`]. + /// /// If decoding fails, the value is inserted as a raw value. fn insert_mapping_storage_values( &mut self, @@ -455,6 +457,7 @@ impl FuzzDictionary { if values.len() < limit as usize { values.insert(sample_value); } else { + // Insert as state value (will be removed at the end of the run). self.insert_value(sample_value); } } else { From 2a8da889928c9889715105c4adc358204e5b259b Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:28:05 +0530 Subject: [PATCH 12/41] fix tests --- testdata/default/cheats/RecordAccountAccesses.t.sol | 4 ++-- testdata/default/repros/Issue9643.t.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testdata/default/cheats/RecordAccountAccesses.t.sol b/testdata/default/cheats/RecordAccountAccesses.t.sol index 02089b5335358..1ecd2c04de18d 100644 --- a/testdata/default/cheats/RecordAccountAccesses.t.sol +++ b/testdata/default/cheats/RecordAccountAccesses.t.sol @@ -264,12 +264,12 @@ contract RecordAccountAccessesTest is DSTest { string memory diffs = cheats.getStateDiff(); assertEq( - "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9\ncontract: default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\n- state diff:\n@ 0x00000000000000000000000000000000000000000000000000000000000004d3: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x000000000000000000000000000000000000000000000000000000000000162e\n\n0xc7183455a4C133Ae270771860664b6B7ec320bB1\n- state diff:\n@ 0x000000000000000000000000000000000000000000000000000000000000162e: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x00000000000000000000000000000000000000000000000000000000000004d2\n\n", + "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9\ncontract: default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\n- state diff:\n@ 0x00000000000000000000000000000000000000000000000000000000000004d3: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x000000000000000000000000000000000000000000000000000000000000162e\n\n0xc7183455a4C133Ae270771860664b6B7ec320bB1\ncontract: default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\n- state diff:\n@ 0x000000000000000000000000000000000000000000000000000000000000162e: 0x0000000000000000000000000000000000000000000000000000000000000000 \xE2\x86\x92 0x00000000000000000000000000000000000000000000000000000000000004d2\n\n", diffs ); string memory diffsJson = cheats.getStateDiffJson(); assertEq( - "{\"0x5991a2df15a8f6a256d3ec51e99254cd3fb576a9\":{\"label\":null,\"contract\":\"default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\",\"balanceDiff\":null,\"stateDiff\":{\"0x00000000000000000000000000000000000000000000000000000000000004d3\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x000000000000000000000000000000000000000000000000000000000000162e\"}}},\"0xc7183455a4c133ae270771860664b6b7ec320bb1\":{\"label\":null,\"balanceDiff\":null,\"stateDiff\":{\"0x000000000000000000000000000000000000000000000000000000000000162e\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x00000000000000000000000000000000000000000000000000000000000004d2\"}}}}", + "{\"0x5991a2df15a8f6a256d3ec51e99254cd3fb576a9\":{\"label\":null,\"contract\":\"default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\",\"balanceDiff\":null,\"stateDiff\":{\"0x00000000000000000000000000000000000000000000000000000000000004d3\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x000000000000000000000000000000000000000000000000000000000000162e\"}}},\"0xc7183455a4c133ae270771860664b6b7ec320bb1\":{\"label\":null,\"contract\":\"default/cheats/RecordAccountAccesses.t.sol:StorageAccessor\",\"balanceDiff\":null,\"stateDiff\":{\"0x000000000000000000000000000000000000000000000000000000000000162e\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x00000000000000000000000000000000000000000000000000000000000004d2\"}}}}", diffsJson ); Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff()); diff --git a/testdata/default/repros/Issue9643.t.sol b/testdata/default/repros/Issue9643.t.sol index 7a985138dc335..27afe3048fbc8 100644 --- a/testdata/default/repros/Issue9643.t.sol +++ b/testdata/default/repros/Issue9643.t.sol @@ -42,7 +42,7 @@ contract Issue9643Test is DSTest { proxied.setCounter(42); string memory rawDiff = vm.getStateDiffJson(); assertEq( - "{\"0x2e234dae75c793f67a35089c9d99245e1c58470b\":{\"label\":null,\"balanceDiff\":null,\"stateDiff\":{\"0x0000000000000000000000000000000000000000000000000000000000000000\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x000000000000000000000000000000000000000000000000000000000000002a\"}}}}", + "{\"0x2e234dae75c793f67a35089c9d99245e1c58470b\":{\"label\":null,\"contract\":\"default/repros/Issue9643.t.sol:DelegateProxy\",\"balanceDiff\":null,\"stateDiff\":{\"0x0000000000000000000000000000000000000000000000000000000000000000\":{\"previousValue\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"newValue\":\"0x000000000000000000000000000000000000000000000000000000000000002a\"}}}}", rawDiff ); } From 3c74ed996d20b1e02e373062debb9e197988dea5 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:39:22 +0530 Subject: [PATCH 13/41] feat(`cheatcodes`): include `SlotInfo` in SlotStateDiff --- crates/cheatcodes/src/evm.rs | 150 +++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 6 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index d090ce53f5786..b88a113ddfc05 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -103,6 +103,18 @@ struct SlotStateDiff { previous_value: B256, /// Current storage value. new_value: B256, + /// Slot Info + #[serde(skip_serializing_if = "Option::is_none", flatten)] + slot_info: Option, +} + +#[derive(Serialize, Default, Debug)] +struct SlotInfo { + label: String, + #[serde(rename = "type")] + storage_type: String, + offset: i64, + slot: String, } /// Balance diff info. @@ -152,11 +164,23 @@ impl Display for AccountStateDiffs { 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 - )?; + // If we have storage layout info, include it + if let Some(slot_info) = &slot_changes.slot_info { + writeln!( + f, + "@ {slot} ({}, {}): {} → {}", + slot_info.label, + slot_info.storage_type, + slot_changes.previous_value, + slot_changes.new_value + )?; + } else { + writeln!( + f, + "@ {slot}: {} → {}", + slot_changes.previous_value, slot_changes.new_value + )?; + } } } @@ -1225,12 +1249,31 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap BTreeMap { + // Get storage layout info for this slot if available + let slot_info = storage_layouts + .get(&storage_access.account) + .and_then(|layout| { + tracing::debug!( + "Looking for slot {} in layout for account {}", + storage_access.slot, + storage_access.account + ); + get_slot_info(layout, &storage_access.slot) + }); + + if slot_info.is_some() { + tracing::info!( + "Found slot info for slot {} at account {}: {:?}", + storage_access.slot, + storage_access.account, + slot_info + ); + } else { + tracing::warn!( + "No slot info found for slot {} at account {}", + storage_access.slot, + storage_access.account + ); + } + slot_state_diff.insert(SlotStateDiff { previous_value: storage_access.previousValue, new_value: storage_access.newValue, + slot_info, }); } Entry::Occupied(mut slot_state_diff) => { @@ -1321,6 +1392,73 @@ fn get_contract_name(ccx: &mut CheatsCtxt, address: Address) -> Option { None } +/// Helper function to get the contract data from the deployed code at an address. +fn get_contract_data<'a>( + ccx: &'a mut CheatsCtxt, + address: Address, +) -> Option<(&'a foundry_compilers::ArtifactId, &'a foundry_common::contracts::ContractData)> { + // Check if we have available artifacts to match against + let artifacts = ccx.state.config.available_artifacts.as_ref()?; + + // Try to load the account and get its code + let account = ccx.ecx.journaled_state.load_account(address).ok()?; + let code = account.info.code.as_ref()?; + + // Skip if code is empty + if code.is_empty() { + return None; + } + + // Try to find the artifact by deployed code + let code_bytes = code.original_bytes(); + if let Some(result) = artifacts.find_by_deployed_code_exact(&code_bytes) { + return Some(result); + } + + // Fallback to fuzzy matching if exact match fails + artifacts.find_by_deployed_code(&code_bytes) +} + +/// Helper function to get storage layout info for a specific slot. +fn get_slot_info( + storage_layout: &foundry_compilers::artifacts::StorageLayout, + slot: &B256, +) -> Option { + // Convert B256 slot to decimal string for comparison with storage layout + // Storage layout stores slots as decimal strings like "0", "1", "2" + let slot_value = U256::from_be_bytes(slot.0); + let slot_str = slot_value.to_string(); + + tracing::debug!( + "get_slot_info: Looking for slot {} in layout with {} entries", + slot_str, + storage_layout.storage.len() + ); + + for entry in &storage_layout.storage { + tracing::debug!(" Storage entry: slot={}, label={}", entry.slot, entry.label); + } + + // Find the storage entry that matches this slot + for storage in &storage_layout.storage { + if storage.slot == slot_str { + // Get the type information if available + let storage_type = storage_layout.types.get(&storage.storage_type); + + return Some(SlotInfo { + label: storage.label.clone(), + storage_type: storage_type + .map(|t| t.label.clone()) + .unwrap_or_else(|| storage.storage_type.clone()), + offset: storage.offset, + slot: storage.slot.clone(), + }); + } + } + + None +} + /// Helper function to set / unset cold storage slot of the target address. fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) { if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) From 85a225bd17c75c6fb7829841028dbb87b821bd9c Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:21:46 +0530 Subject: [PATCH 14/41] cleanup + identify slots of static arrays --- crates/cheatcodes/src/evm.rs | 171 ++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 53 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index b88a113ddfc05..55697675156e5 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -11,6 +11,7 @@ use alloy_primitives::{Address, B256, U256, map::HashMap}; use alloy_rlp::Decodable; use alloy_sol_types::SolValue; use foundry_common::fs::{read_json_file, write_json_file}; +use foundry_compilers::artifacts::{Storage, StorageLayout}; use foundry_evm_core::{ ContextExt, backend::{DatabaseExt, RevertStateSnapshotAction}, @@ -30,6 +31,7 @@ use std::{ collections::{BTreeMap, HashSet, btree_map::Entry}, fmt::Display, path::Path, + str::FromStr, }; mod record_debug_step; @@ -1258,19 +1260,8 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap BTreeMap( artifacts.find_by_deployed_code(&code_bytes) } -/// Helper function to get storage layout info for a specific slot. -fn get_slot_info( - storage_layout: &foundry_compilers::artifacts::StorageLayout, - slot: &B256, +/// Helper function to check if a storage entry matches the given slot exactly. +fn check_exact_slot_match( + storage: &Storage, + storage_layout: &StorageLayout, + slot_str: &str, ) -> Option { + trace!(type_label = %storage.storage_type, "checking type"); + if storage.slot == slot_str { + let storage_type = storage_layout.types.get(&storage.storage_type); + return Some(SlotInfo { + label: storage.label.clone(), + storage_type: storage_type + .map(|t| t.label.clone()) + .unwrap_or_else(|| storage.storage_type.clone()), + offset: storage.offset, + slot: storage.slot.clone(), + }); + } + None +} + +/// Helper function to check if a slot is part of a static array. +fn check_array_slot_match( + storage: &Storage, + type_info: &StorageType, + base_slot: U256, + slot_value: U256, + slot_str: &str, +) -> Option { + // Check for static arrays by looking for encoding == "inplace" and array syntax in label + trace!(type_label = %type_info.label, "checking if type is a static_array"); + let is_static_array = type_info.encoding == "inplace" + && type_info.label.contains('[') + && type_info.label.contains(']'); + + if !is_static_array { + return None; + } + + // For arrays, calculate the number of slots based on total size + let Ok(total_bytes) = type_info.number_of_bytes.parse::() else { + return None; + }; + + // Each slot is 32 bytes + let total_slots = (total_bytes + 31) / 32; + + // Check if current slot is within the array range + if slot_value <= base_slot || slot_value >= base_slot + U256::from(total_slots) { + return None; + } + + // Calculate index based on slot offset + let slot_offset = (slot_value - base_slot).to::(); + + // Extract array size from label to calculate proper index + let array_size = extract_array_size(&type_info.label)?; + let slots_per_element = total_slots / array_size; + let index = slot_offset / slots_per_element; + + Some(SlotInfo { + label: format!("{}[{}]", storage.label, index), + storage_type: type_info.label.clone(), + offset: 0, // Array elements always start at offset 0 + slot: slot_str.to_string(), + }) +} + +/// Helper function to get storage layout info for a specific slot. +fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { // Convert B256 slot to decimal string for comparison with storage layout // Storage layout stores slots as decimal strings like "0", "1", "2" let slot_value = U256::from_be_bytes(slot.0); let slot_str = slot_value.to_string(); - tracing::debug!( - "get_slot_info: Looking for slot {} in layout with {} entries", - slot_str, - storage_layout.storage.len() - ); + // Single loop to check both exact matches and array ranges + for storage in &storage_layout.storage { + // Parse base slot as U256 to handle large slot numbers + let Ok(base_slot) = U256::from_str(&storage.slot) else { + continue; + }; - for entry in &storage_layout.storage { - tracing::debug!(" Storage entry: slot={}, label={}", entry.slot, entry.label); - } + // First check for exact match + if let Some(slot_info) = check_exact_slot_match(storage, storage_layout, &slot_str) { + return Some(slot_info); + } - // Find the storage entry that matches this slot - for storage in &storage_layout.storage { - if storage.slot == slot_str { - // Get the type information if available - let storage_type = storage_layout.types.get(&storage.storage_type); - - return Some(SlotInfo { - label: storage.label.clone(), - storage_type: storage_type - .map(|t| t.label.clone()) - .unwrap_or_else(|| storage.storage_type.clone()), - offset: storage.offset, - slot: storage.slot.clone(), - }); + // Check if this is a static array with inplace encoding (contiguous storage) + if let Some(type_info) = storage_layout.types.get(&storage.storage_type) { + if let Some(slot_info) = + check_array_slot_match(storage, type_info, base_slot, slot_value, &slot_str) + { + return Some(slot_info); + } } } None } +/// Helper function to extract array size from a type string like "uint256[3]" +fn extract_array_size(type_str: &str) -> Option { + if let Some(start) = type_str.rfind('[') { + if let Some(end) = type_str.rfind(']') { + if let Ok(size) = type_str[start + 1..end].parse::() { + return Some(size); + } + } + } + None +} + /// Helper function to set / unset cold storage slot of the target address. fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) { if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) @@ -1467,3 +1515,20 @@ fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) storage_slot.is_cold = cold; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_array_size() { + assert_eq!(extract_array_size("uint256[3]"), Some(3)); + assert_eq!(extract_array_size("address[10]"), Some(10)); + assert_eq!(extract_array_size("bool[5]"), Some(5)); + assert_eq!(extract_array_size("bytes32[100]"), Some(100)); + assert_eq!(extract_array_size("uint256"), None); + assert_eq!(extract_array_size("mapping(address => uint256)"), None); + assert_eq!(extract_array_size("uint256[]"), None); // Dynamic array + assert_eq!(extract_array_size("struct[42]"), Some(42)); + } +} From a4228eff0bd507b1cb5d59820adfb84ed607a0db Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:38:31 +0530 Subject: [PATCH 15/41] nits --- crates/cheatcodes/src/evm.rs | 38 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 55697675156e5..7679ec6e058bc 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -11,7 +11,7 @@ use alloy_primitives::{Address, B256, U256, map::HashMap}; use alloy_rlp::Decodable; use alloy_sol_types::SolValue; use foundry_common::fs::{read_json_file, write_json_file}; -use foundry_compilers::artifacts::{Storage, StorageLayout}; +use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType}; use foundry_evm_core::{ ContextExt, backend::{DatabaseExt, RevertStateSnapshotAction}, @@ -1421,8 +1421,7 @@ fn check_array_slot_match( storage: &Storage, type_info: &StorageType, base_slot: U256, - slot_value: U256, - slot_str: &str, + slot: U256, ) -> Option { // Check for static arrays by looking for encoding == "inplace" and array syntax in label trace!(type_label = %type_info.label, "checking if type is a static_array"); @@ -1443,12 +1442,12 @@ fn check_array_slot_match( let total_slots = (total_bytes + 31) / 32; // Check if current slot is within the array range - if slot_value <= base_slot || slot_value >= base_slot + U256::from(total_slots) { + if slot <= base_slot || slot >= base_slot + U256::from(total_slots) { return None; } // Calculate index based on slot offset - let slot_offset = (slot_value - base_slot).to::(); + let slot_offset = (slot - base_slot).to::(); // Extract array size from label to calculate proper index let array_size = extract_array_size(&type_info.label)?; @@ -1458,8 +1457,8 @@ fn check_array_slot_match( Some(SlotInfo { label: format!("{}[{}]", storage.label, index), storage_type: type_info.label.clone(), - offset: 0, // Array elements always start at offset 0 - slot: slot_str.to_string(), + offset: 0, + slot: slot.to_string(), }) } @@ -1467,8 +1466,8 @@ fn check_array_slot_match( fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { // Convert B256 slot to decimal string for comparison with storage layout // Storage layout stores slots as decimal strings like "0", "1", "2" - let slot_value = U256::from_be_bytes(slot.0); - let slot_str = slot_value.to_string(); + let slot = U256::from_be_bytes(slot.0); + let slot_str = slot.to_string(); // Single loop to check both exact matches and array ranges for storage in &storage_layout.storage { @@ -1483,12 +1482,10 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Option Option { - if let Some(start) = type_str.rfind('[') { - if let Some(end) = type_str.rfind(']') { - if let Ok(size) = type_str[start + 1..end].parse::() { - return Some(size); - } - } + if let Some(start) = type_str.rfind('[') + && let Some(end) = type_str.rfind(']') + && let Ok(size) = type_str[start + 1..end].parse::() + { + return Some(size); } None } From 1853d8ed44008ef8b75476487787b155f42d41d4 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:39:37 +0530 Subject: [PATCH 16/41] nit --- crates/cheatcodes/src/evm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 7679ec6e058bc..71a587c50974c 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1525,6 +1525,6 @@ mod tests { assert_eq!(extract_array_size("uint256"), None); assert_eq!(extract_array_size("mapping(address => uint256)"), None); assert_eq!(extract_array_size("uint256[]"), None); // Dynamic array - assert_eq!(extract_array_size("struct[42]"), Some(42)); + assert_eq!(extract_array_size("CustomStruct[42]"), Some(42)); } } From 5b8b95bca2c78b5d708a15c849afe47afc626627 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:48:05 +0530 Subject: [PATCH 17/41] nits --- crates/cheatcodes/src/evm.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 71a587c50974c..c79efda09f8db 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1404,8 +1404,23 @@ fn check_exact_slot_match( trace!(type_label = %storage.storage_type, "checking type"); if storage.slot == slot_str { let storage_type = storage_layout.types.get(&storage.storage_type); + + // Check if this is a static array - if so, label the base slot as element [0] + let label = if let Some(type_info) = storage_type { + if type_info.encoding == "inplace" + && type_info.label.contains('[') + && type_info.label.contains(']') + { + format!("{}[0]", storage.label) + } else { + storage.label.clone() + } + } else { + storage.label.clone() + }; + return Some(SlotInfo { - label: storage.label.clone(), + label, storage_type: storage_type .map(|t| t.label.clone()) .unwrap_or_else(|| storage.storage_type.clone()), From e619e0d7e091474151cb1c5b3afeb0cd09e56480 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:11:11 +0530 Subject: [PATCH 18/41] test + nits --- crates/cheatcodes/src/evm.rs | 10 +- crates/forge/tests/it/cheats.rs | 37 ++- .../cheats/StateDiffStorageLayout.t.sol | 217 ++++++++++++++++++ 3 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 testdata/default/cheats/StateDiffStorageLayout.t.sol diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index c79efda09f8db..9c65593565f64 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1260,10 +1260,10 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap= base_slot + U256::from(total_slots) { diff --git a/crates/forge/tests/it/cheats.rs b/crates/forge/tests/it/cheats.rs index 5e2764c603fd0..daadd874b9593 100644 --- a/crates/forge/tests/it/cheats.rs +++ b/crates/forge/tests/it/cheats.rs @@ -1,22 +1,23 @@ //! Forge tests for cheatcodes. - use crate::{ config::*, test_helpers::{ - ForgeTestData, RE_PATH_SEPARATOR, TEST_DATA_DEFAULT, TEST_DATA_MULTI_VERSION, - TEST_DATA_PARIS, + ForgeTestData, ForgeTestProfile, RE_PATH_SEPARATOR, TEST_DATA_DEFAULT, + TEST_DATA_MULTI_VERSION, TEST_DATA_PARIS, get_compiled, }, }; use alloy_primitives::U256; +use foundry_cli::utils::install_crypto_provider; +use foundry_compilers::artifacts::output_selection::ContractOutputSelection; use foundry_config::{FsPermissions, fs_permissions::PathPermission}; -use foundry_test_utils::Filter; +use foundry_test_utils::{Filter, init_tracing}; /// Executes all cheat code tests but not fork cheat codes or tests that require isolation mode or /// specific seed. async fn test_cheats_local(test_data: &ForgeTestData) { let mut filter = Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}*")) .exclude_paths("Fork") - .exclude_contracts("(Isolated|WithSeed)"); + .exclude_contracts("(Isolated|WithSeed|StateDiffStorageLayoutTest)"); // Exclude FFI tests on Windows because no `echo`, and file tests that expect certain file paths if cfg!(windows) { @@ -36,7 +37,8 @@ async fn test_cheats_local(test_data: &ForgeTestData) { /// Executes subset of all cheat code tests in isolation mode. async fn test_cheats_local_isolated(test_data: &ForgeTestData) { - let filter = Filter::new(".*", ".*(Isolated)", &format!(".*cheats{RE_PATH_SEPARATOR}*")); + let filter = Filter::new(".*", ".*(Isolated)", &format!(".*cheats{RE_PATH_SEPARATOR}*")) + .exclude_contracts("StateDiffStorageLayoutTest"); let runner = test_data.runner_with(|config| { config.isolate = true; @@ -61,6 +63,29 @@ async fn test_cheats_local_default() { test_cheats_local(&TEST_DATA_DEFAULT).await } +#[tokio::test(flavor = "multi_thread")] +async fn test_state_diff_storage_layout() { + let test_data = { + let profile = ForgeTestProfile::Default; + install_crypto_provider(); + init_tracing(); + let mut config = profile.config(); + config.extra_output = vec![ContractOutputSelection::StorageLayout]; + let mut project = config.project().unwrap(); + // Compile with StorageLayout + let output = get_compiled(&mut project); + ForgeTestData { project, output, config: config.into(), profile } + }; + let filter = + Filter::new(".*", "StateDiffStorageLayoutTest", &format!(".*cheats{RE_PATH_SEPARATOR}*")); + + let runner = test_data.runner_with(|config| { + config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); + }); + + TestConfig::with_filter(runner, filter).run().await; +} + #[tokio::test(flavor = "multi_thread")] async fn test_cheats_local_default_isolated() { test_cheats_local_isolated(&TEST_DATA_DEFAULT).await diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol new file mode 100644 index 0000000000000..4f147f8df410c --- /dev/null +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract SimpleStorage { + uint256 public value; // Slot 0 + address public owner; // Slot 1 + uint256[3] public values; // Slots 2, 3, 4 + + constructor() { + owner = msg.sender; + } + + function setValue(uint256 _value) public { + value = _value; + } + + function setOwner(address _owner) public { + owner = _owner; + } + + function setValues(uint256 a, uint256 b, uint256 c) public { + values[0] = a; + values[1] = b; + values[2] = c; + } +} + +contract VariousArrays { + // Different array types to test + uint256[3] public numbers; // Slots 0, 1, 2 + address[2] public addresses; // Slots 3, 4 + bool[5] public flags; // Slot 5 (packed) + bytes32[2] public hashes; // Slots 6, 7 + + function setNumbers(uint256 a, uint256 b, uint256 c) public { + numbers[0] = a; + numbers[1] = b; + numbers[2] = c; + } + + function setAddresses(address a, address b) public { + addresses[0] = a; + addresses[1] = b; + } + + function setFlags(bool a, bool b, bool c, bool d, bool e) public { + flags[0] = a; + flags[1] = b; + flags[2] = c; + flags[3] = d; + flags[4] = e; + } + + function setHashes(bytes32 a, bytes32 b) public { + hashes[0] = a; + hashes[1] = b; + } +} + +contract StateDiffStorageLayoutTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + SimpleStorage simpleStorage; + VariousArrays variousArrays; + + function setUp() public { + simpleStorage = new SimpleStorage(); + variousArrays = new VariousArrays(); + } + + function testSimpleStorageLayout() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Modify storage slots with known positions + simpleStorage.setValue(42); // Modifies slot 0 + simpleStorage.setOwner(address(this)); // Modifies slot 1 + simpleStorage.setValues(100, 200, 300); // Modifies slots 2, 3, 4 + + // Get the state diff as string + string memory stateDiff = vm.getStateDiff(); + + // Get the state diff as JSON and verify it contains the expected structure + string memory stateDiffJson = vm.getStateDiffJson(); + + // The JSON should contain storage layout info for all slots + // We check the JSON contains expected substrings for the labels and types + assertContains(stateDiffJson, "\"label\":\"value\"", "Should contain 'value' label"); + assertContains(stateDiffJson, "\"label\":\"owner\"", "Should contain 'owner' label"); + assertContains(stateDiffJson, "\"label\":\"values[0]\"", "Should contain 'values[0]' label"); + assertContains(stateDiffJson, "\"label\":\"values[1]\"", "Should contain 'values[1]' label"); + assertContains(stateDiffJson, "\"label\":\"values[2]\"", "Should contain 'values[2]' label"); + + assertContains(stateDiffJson, "\"type\":\"uint256\"", "Should contain uint256 type"); + assertContains(stateDiffJson, "\"type\":\"address\"", "Should contain address type"); + assertContains(stateDiffJson, "\"type\":\"uint256[3]\"", "Should contain uint256[3] type"); + + // Stop recording and verify we get the expected account accesses + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + assertTrue(accesses.length >= 3, "Should have at least 3 account accesses for the calls"); + + // Verify storage accesses for SimpleStorage + bool foundValueSlot = false; + bool foundOwnerSlot = false; + bool foundValuesSlot0 = false; + bool foundValuesSlot1 = false; + bool foundValuesSlot2 = false; + + for (uint256 i = 0; i < accesses.length; i++) { + if (accesses[i].account == address(simpleStorage)) { + for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + bytes32 slot = accesses[i].storageAccesses[j].slot; + if (slot == bytes32(uint256(0))) foundValueSlot = true; + if (slot == bytes32(uint256(1))) foundOwnerSlot = true; + if (slot == bytes32(uint256(2))) foundValuesSlot0 = true; + if (slot == bytes32(uint256(3))) foundValuesSlot1 = true; + if (slot == bytes32(uint256(4))) foundValuesSlot2 = true; + } + } + } + + assertTrue(foundValueSlot, "Should have accessed slot 0 (value)"); + assertTrue(foundOwnerSlot, "Should have accessed slot 1 (owner)"); + assertTrue(foundValuesSlot0, "Should have accessed slot 2 (values[0])"); + assertTrue(foundValuesSlot1, "Should have accessed slot 3 (values[1])"); + assertTrue(foundValuesSlot2, "Should have accessed slot 4 (values[2])"); + } + + function testVariousArrayTypes() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Modify different array types + variousArrays.setNumbers(100, 200, 300); + variousArrays.setAddresses(address(0x1), address(0x2)); + variousArrays.setFlags(true, false, true, false, true); + variousArrays.setHashes(keccak256("test1"), keccak256("test2")); + + // Get the state diff as JSON + string memory stateDiffJson = vm.getStateDiffJson(); + + // Verify all array types are properly labeled with indices + assertContains(stateDiffJson, "\"label\":\"numbers[0]\"", "Should contain 'numbers[0]' label"); + assertContains(stateDiffJson, "\"label\":\"numbers[1]\"", "Should contain 'numbers[1]' label"); + assertContains(stateDiffJson, "\"label\":\"numbers[2]\"", "Should contain 'numbers[2]' label"); + + assertContains(stateDiffJson, "\"label\":\"addresses[0]\"", "Should contain 'addresses[0]' label"); + assertContains(stateDiffJson, "\"label\":\"addresses[1]\"", "Should contain 'addresses[1]' label"); + + assertContains(stateDiffJson, "\"label\":\"flags[0]\"", "Should contain 'flags[0]' label"); + + assertContains(stateDiffJson, "\"label\":\"hashes[0]\"", "Should contain 'hashes[0]' label"); + assertContains(stateDiffJson, "\"label\":\"hashes[1]\"", "Should contain 'hashes[1]' label"); + + // Verify types are correctly identified + assertContains(stateDiffJson, "\"type\":\"uint256[3]\"", "Should contain uint256[3] type"); + assertContains(stateDiffJson, "\"type\":\"address[2]\"", "Should contain address[2] type"); + assertContains(stateDiffJson, "\"type\":\"bool[5]\"", "Should contain bool[5] type"); + assertContains(stateDiffJson, "\"type\":\"bytes32[2]\"", "Should contain bytes32[2] type"); + + // Stop recording and verify account accesses + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + assertTrue(accesses.length > 0, "Should have account accesses"); + } + + function testStateDiffJsonFormat() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Make a simple change to verify JSON format + simpleStorage.setValue(123); + + // Get the JSON and verify it's properly formatted + string memory stateDiffJson = vm.getStateDiffJson(); + + // Check JSON structure contains expected fields + assertContains(stateDiffJson, "\"previousValue\":", "JSON should contain previousValue field"); + assertContains(stateDiffJson, "\"newValue\":", "JSON should contain newValue field"); + assertContains(stateDiffJson, "\"label\":", "JSON should contain label field"); + assertContains(stateDiffJson, "\"type\":", "JSON should contain type field"); + assertContains(stateDiffJson, "\"offset\":", "JSON should contain offset field"); + assertContains(stateDiffJson, "\"slot\":", "JSON should contain slot field"); + + vm.stopAndReturnStateDiff(); + } + + // Helper function to check if a string contains a substring + function assertContains(string memory haystack, string memory needle, string memory message) internal pure { + bytes memory haystackBytes = bytes(haystack); + bytes memory needleBytes = bytes(needle); + + if (needleBytes.length > haystackBytes.length) { + revert(message); + } + + bool found = false; + for (uint256 i = 0; i <= haystackBytes.length - needleBytes.length; i++) { + bool isMatch = true; + for (uint256 j = 0; j < needleBytes.length; j++) { + if (haystackBytes[i + j] != needleBytes[j]) { + isMatch = false; + break; + } + } + if (isMatch) { + found = true; + break; + } + } + + if (!found) { + revert(message); + } + } +} From caf21b98b49148f4e09085133b23a800747254f5 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:14:07 +0530 Subject: [PATCH 19/41] docs --- crates/cheatcodes/src/evm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 9c65593565f64..fc09511104649 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1507,7 +1507,7 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Option { if let Some(start) = type_str.rfind('[') && let Some(end) = type_str.rfind(']') From e9630a02b8e43eeb9e18a16ec2580e8126596fbb Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:45:38 +0530 Subject: [PATCH 20/41] handle 2d arrays --- crates/cheatcodes/src/evm.rs | 84 +++++++++++--- .../cheats/StateDiffStorageLayout.t.sol | 103 ++++++++++++++++++ 2 files changed, 173 insertions(+), 14 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index fc09511104649..64609334660e0 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1405,13 +1405,24 @@ fn check_exact_slot_match( if storage.slot == slot_str { let storage_type = storage_layout.types.get(&storage.storage_type); - // Check if this is a static array - if so, label the base slot as element [0] + // Check if this is a static array - if so, label the base slot as element [0] or [0][0] let label = if let Some(type_info) = storage_type { if type_info.encoding == "inplace" && type_info.label.contains('[') && type_info.label.contains(']') { - format!("{}[0]", storage.label) + // Check if it's a 2D array + if let Some((_, inner_size)) = extract_array_dimensions(&type_info.label) { + if inner_size > 0 { + // 2D array: label as [0][0] + format!("{}[0][0]", storage.label) + } else { + // 1D array: label as [0] + format!("{}[0]", storage.label) + } + } else { + storage.label.clone() + } } else { storage.label.clone() } @@ -1464,13 +1475,26 @@ fn check_array_slot_match( // Calculate index based on slot offset let slot_offset = (slot - base_slot).to::(); - // Extract array size from label to calculate proper index - let array_size = extract_array_size(&type_info.label)?; - let slots_per_element = total_slots / array_size; - let index = slot_offset / slots_per_element; + // Extract array dimensions to handle both 1D and 2D arrays + let (outer_size, inner_size) = extract_array_dimensions(&type_info.label)?; + + let label = if inner_size > 0 { + // 2D array: calculate indices + // For uint256[3][2], we have 2 outer arrays of 3 elements each + // Slots are laid out as: [0][0], [0][1], [0][2], [1][0], [1][1], [1][2] + let elements_per_outer = inner_size; + let outer_index = slot_offset / elements_per_outer; + let inner_index = slot_offset % elements_per_outer; + format!("{}[{}][{}]", storage.label, outer_index, inner_index) + } else { + // 1D array + let slots_per_element = total_slots / outer_size; + let index = slot_offset / slots_per_element; + format!("{}[{}]", storage.label, index) + }; Some(SlotInfo { - label: format!("{}[{}]", storage.label, index), + label, storage_type: type_info.label.clone(), offset: 0, slot: slot.to_string(), @@ -1507,17 +1531,49 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Option { - if let Some(start) = type_str.rfind('[') - && let Some(end) = type_str.rfind(']') - && let Ok(size) = type_str[start + 1..end].parse::() - { - return Some(size); +/// Helper function to extract array dimensions from a type string +/// Returns (outer_size, inner_size) for 2D arrays, or (size, 0) for 1D arrays +fn extract_array_dimensions(type_str: &str) -> Option<(u64, u64)> { + // Count brackets to determine if it's 1D or 2D + let bracket_pairs: Vec<(usize, usize)> = type_str + .char_indices() + .filter_map(|(i, c)| { + if c == '[' { + // Find matching ']' + type_str[i+1..].find(']').map(|j| (i, i+1+j)) + } else { + None + } + }) + .collect(); + + if bracket_pairs.is_empty() { + return None; } + + if bracket_pairs.len() == 1 { + // 1D array like uint256[3] + let (start, end) = bracket_pairs[0]; + if let Ok(size) = type_str[start + 1..end].parse::() { + return Some((size, 0)); + } + } else if bracket_pairs.len() == 2 { + // 2D array like uint256[3][2] + // In Solidity, uint256[3][2] means 2 arrays of 3 elements each + // The outer dimension is 2, inner dimension is 3 + let (inner_start, inner_end) = bracket_pairs[0]; + let (outer_start, outer_end) = bracket_pairs[1]; + + if let Ok(inner_size) = type_str[inner_start + 1..inner_end].parse::() + && let Ok(outer_size) = type_str[outer_start + 1..outer_end].parse::() { + return Some((outer_size, inner_size)); + } + } + None } + /// Helper function to set / unset cold storage slot of the target address. fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) { if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 4f147f8df410c..77d40bb8bfa69 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -60,14 +60,53 @@ contract VariousArrays { } } +contract TwoDArrayStorage { + // 2D array: 2 arrays of 3 uint256 elements each + // Total slots: 6 (slots 0-5) + // [0][0] at slot 0, [0][1] at slot 1, [0][2] at slot 2 + // [1][0] at slot 3, [1][1] at slot 4, [1][2] at slot 5 + uint256[3][2] public matrix; + + // Another 2D array starting at slot 6 + // 3 arrays of 2 addresses each + // Total slots: 6 (slots 6-11) + address[2][3] public addresses2D; + + // Mixed size 2D array starting at slot 12 + // 4 arrays of 2 bytes32 each + // Total slots: 8 (slots 12-19) + bytes32[2][4] public data2D; + + function setMatrix(uint256[3] memory row0, uint256[3] memory row1) public { + matrix[0] = row0; + matrix[1] = row1; + } + + function setMatrixElement(uint256 i, uint256 j, uint256 value) public { + matrix[i][j] = value; + } + + function setAddresses2D(address[2] memory row0, address[2] memory row1, address[2] memory row2) public { + addresses2D[0] = row0; + addresses2D[1] = row1; + addresses2D[2] = row2; + } + + function setData2D(uint256 i, uint256 j, bytes32 value) public { + data2D[i][j] = value; + } +} + contract StateDiffStorageLayoutTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); SimpleStorage simpleStorage; VariousArrays variousArrays; + TwoDArrayStorage twoDArrayStorage; function setUp() public { simpleStorage = new SimpleStorage(); variousArrays = new VariousArrays(); + twoDArrayStorage = new TwoDArrayStorage(); } function testSimpleStorageLayout() public { @@ -186,6 +225,70 @@ contract StateDiffStorageLayoutTest is DSTest { vm.stopAndReturnStateDiff(); } + function test2DArrayStorageLayout() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Set matrix values + // matrix[0][0] = 100, matrix[0][1] = 101, matrix[0][2] = 102 + // matrix[1][0] = 200, matrix[1][1] = 201, matrix[1][2] = 202 + uint256[3] memory row0 = [uint256(100), 101, 102]; + uint256[3] memory row1 = [uint256(200), 201, 202]; + twoDArrayStorage.setMatrix(row0, row1); + + // Get the state diff and check labels + string memory stateDiffJson = vm.getStateDiffJson(); + + // Verify the labels for 2D array elements + assertContains(stateDiffJson, "\"label\":\"matrix[0][0]\"", "Should contain matrix[0][0] label"); + assertContains(stateDiffJson, "\"label\":\"matrix[0][1]\"", "Should contain matrix[0][1] label"); + assertContains(stateDiffJson, "\"label\":\"matrix[0][2]\"", "Should contain matrix[0][2] label"); + assertContains(stateDiffJson, "\"label\":\"matrix[1][0]\"", "Should contain matrix[1][0] label"); + assertContains(stateDiffJson, "\"label\":\"matrix[1][1]\"", "Should contain matrix[1][1] label"); + assertContains(stateDiffJson, "\"label\":\"matrix[1][2]\"", "Should contain matrix[1][2] label"); + + // Check that we have the right type + assertContains(stateDiffJson, "\"type\":\"uint256[3][2]\"", "Should contain 2D array type"); + + vm.stopAndReturnStateDiff(); + } + + function testMixed2DArrays() public { + vm.startStateDiffRecording(); + + // Test address 2D array + address[2] memory addrRow0 = [address(0x1), address(0x2)]; + address[2] memory addrRow1 = [address(0x3), address(0x4)]; + address[2] memory addrRow2 = [address(0x5), address(0x6)]; + twoDArrayStorage.setAddresses2D(addrRow0, addrRow1, addrRow2); + + // Test bytes32 2D array + twoDArrayStorage.setData2D(0, 0, keccak256("data00")); + twoDArrayStorage.setData2D(0, 1, keccak256("data01")); + twoDArrayStorage.setData2D(1, 0, keccak256("data10")); + twoDArrayStorage.setData2D(1, 1, keccak256("data11")); + + string memory stateDiffJson = vm.getStateDiffJson(); + + // Check for proper types + assertContains(stateDiffJson, "\"type\":\"address[2][3]\"", "Should contain address 2D array type"); + assertContains(stateDiffJson, "\"type\":\"bytes32[2][4]\"", "Should contain bytes32 2D array type"); + + // Verify address 2D array labels + assertContains(stateDiffJson, "\"label\":\"addresses2D[0][0]\"", "Should contain addresses2D[0][0] label"); + assertContains(stateDiffJson, "\"label\":\"addresses2D[0][1]\"", "Should contain addresses2D[0][1] label"); + assertContains(stateDiffJson, "\"label\":\"addresses2D[1][0]\"", "Should contain addresses2D[1][0] label"); + assertContains(stateDiffJson, "\"label\":\"addresses2D[2][1]\"", "Should contain addresses2D[2][1] label"); + + // Verify data 2D array labels + assertContains(stateDiffJson, "\"label\":\"data2D[0][0]\"", "Should contain data2D[0][0] label"); + assertContains(stateDiffJson, "\"label\":\"data2D[0][1]\"", "Should contain data2D[0][1] label"); + assertContains(stateDiffJson, "\"label\":\"data2D[1][0]\"", "Should contain data2D[1][0] label"); + assertContains(stateDiffJson, "\"label\":\"data2D[1][1]\"", "Should contain data2D[1][1] label"); + + vm.stopAndReturnStateDiff(); + } + // Helper function to check if a string contains a substring function assertContains(string memory haystack, string memory needle, string memory message) internal pure { bytes memory haystackBytes = bytes(haystack); From b44b410cea709954195958bdac86ded1354f222d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:04:36 +0530 Subject: [PATCH 21/41] use DynSolType --- crates/cheatcodes/src/evm.rs | 165 ++++++++++++----------------------- 1 file changed, 55 insertions(+), 110 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 64609334660e0..8d28969a0418e 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -6,6 +6,7 @@ use crate::{ inspector::{Ecx, RecordDebugStepInfo}, }; use alloy_consensus::TxEnvelope; +use alloy_dyn_abi::DynSolType; use alloy_genesis::{Genesis, GenesisAccount}; use alloy_primitives::{Address, B256, U256, map::HashMap}; use alloy_rlp::Decodable; @@ -1403,26 +1404,16 @@ fn check_exact_slot_match( ) -> Option { trace!(type_label = %storage.storage_type, "checking type"); if storage.slot == slot_str { - let storage_type = storage_layout.types.get(&storage.storage_type); - - // Check if this is a static array - if so, label the base slot as element [0] or [0][0] - let label = if let Some(type_info) = storage_type { - if type_info.encoding == "inplace" - && type_info.label.contains('[') - && type_info.label.contains(']') - { - // Check if it's a 2D array - if let Some((_, inner_size)) = extract_array_dimensions(&type_info.label) { - if inner_size > 0 { - // 2D array: label as [0][0] - format!("{}[0][0]", storage.label) - } else { - // 1D array: label as [0] - format!("{}[0]", storage.label) - } - } else { - storage.label.clone() - } + // Get the StorageType which contains the clean label + let storage_type = storage_layout.types.get(&storage.storage_type)?; + let type_label = storage_type.label.clone(); + + // Parse the type to check if it's an array + let label = if let Ok(dyn_type) = DynSolType::parse(&type_label) { + if let DynSolType::FixedArray(_, _) = &dyn_type { + // For arrays, label the base slot with indices + let indices = get_array_base_indices(&dyn_type); + format!("{}{}", storage.label, indices) } else { storage.label.clone() } @@ -1432,9 +1423,7 @@ fn check_exact_slot_match( return Some(SlotInfo { label, - storage_type: storage_type - .map(|t| t.label.clone()) - .unwrap_or_else(|| storage.storage_type.clone()), + storage_type: type_label, offset: storage.offset, slot: storage.slot.clone(), }); @@ -1449,17 +1438,19 @@ fn check_array_slot_match( base_slot: U256, slot: U256, ) -> Option { - // Check for static arrays by looking for encoding == "inplace" and array syntax in label - trace!(type_label = %type_info.label, "checking if type is a static_array"); - let is_static_array = type_info.encoding == "inplace" - && type_info.label.contains('[') - && type_info.label.contains(']'); + // StorageType.label already contains the clean type string without 't_' prefix + let type_label = type_info.label.clone(); - if !is_static_array { + // Parse the type using DynSolType + trace!(type_label = %type_label, "checking if type is a static_array"); + let dyn_type = DynSolType::parse(&type_label).ok()?; + + // Check if it's a static array + if !matches!(dyn_type, DynSolType::FixedArray(_, _)) { return None; } - // For arrays, calculate the number of slots based on total size + // For arrays, calculate the number of slots based on the actual storage size let Ok(total_bytes) = type_info.number_of_bytes.parse::() else { return None; }; @@ -1467,38 +1458,18 @@ fn check_array_slot_match( // Each slot is 32 bytes let total_slots = total_bytes.div_ceil(32); - // Check if current slot is within the array range + // Check if slot is within array range if slot <= base_slot || slot >= base_slot + U256::from(total_slots) { return None; } - // Calculate index based on slot offset - let slot_offset = (slot - base_slot).to::(); - - // Extract array dimensions to handle both 1D and 2D arrays - let (outer_size, inner_size) = extract_array_dimensions(&type_info.label)?; - - let label = if inner_size > 0 { - // 2D array: calculate indices - // For uint256[3][2], we have 2 outer arrays of 3 elements each - // Slots are laid out as: [0][0], [0][1], [0][2], [1][0], [1][1], [1][2] - let elements_per_outer = inner_size; - let outer_index = slot_offset / elements_per_outer; - let inner_index = slot_offset % elements_per_outer; - format!("{}[{}][{}]", storage.label, outer_index, inner_index) - } else { - // 1D array - let slots_per_element = total_slots / outer_size; - let index = slot_offset / slots_per_element; - format!("{}[{}]", storage.label, index) - }; + let index = slot - base_slot; + let index_u64 = index.to::(); - Some(SlotInfo { - label, - storage_type: type_info.label.clone(), - offset: 0, - slot: slot.to_string(), - }) + // Generate the label with the correct index + let label = format_array_element_label(&storage.label, &dyn_type, index_u64); + + Some(SlotInfo { label, storage_type: type_label, offset: 0, slot: slot.to_string() }) } /// Helper function to get storage layout info for a specific slot. @@ -1531,48 +1502,39 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Option<(u64, u64)> { - // Count brackets to determine if it's 1D or 2D - let bracket_pairs: Vec<(usize, usize)> = type_str - .char_indices() - .filter_map(|(i, c)| { - if c == '[' { - // Find matching ']' - type_str[i+1..].find(']').map(|j| (i, i+1+j)) +/// Returns "[0]" for 1D arrays, "[0][0]" for 2D arrays, etc. +fn get_array_base_indices(dyn_type: &DynSolType) -> String { + match dyn_type { + DynSolType::FixedArray(inner, _) => { + if let DynSolType::FixedArray(_, _) = inner.as_ref() { + // Nested array (2D or higher) + format!("[0]{}", get_array_base_indices(inner)) } else { - None + // Simple 1D array + "[0]".to_string() } - }) - .collect(); - - if bracket_pairs.is_empty() { - return None; - } - - if bracket_pairs.len() == 1 { - // 1D array like uint256[3] - let (start, end) = bracket_pairs[0]; - if let Ok(size) = type_str[start + 1..end].parse::() { - return Some((size, 0)); - } - } else if bracket_pairs.len() == 2 { - // 2D array like uint256[3][2] - // In Solidity, uint256[3][2] means 2 arrays of 3 elements each - // The outer dimension is 2, inner dimension is 3 - let (inner_start, inner_end) = bracket_pairs[0]; - let (outer_start, outer_end) = bracket_pairs[1]; - - if let Ok(inner_size) = type_str[inner_start + 1..inner_end].parse::() - && let Ok(outer_size) = type_str[outer_start + 1..outer_end].parse::() { - return Some((outer_size, inner_size)); } + _ => String::new(), } - - None } +/// Helper function to format an array element label given its index +fn format_array_element_label(base_label: &str, dyn_type: &DynSolType, index: u64) -> String { + match dyn_type { + DynSolType::FixedArray(inner, _size) => { + if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() { + // 2D array: calculate row and column + let row = index / (*inner_size as u64); + let col = index % (*inner_size as u64); + format!("{base_label}[{row}][{col}]") + } else { + // 1D array + format!("{base_label}[{index}]") + } + } + _ => base_label.to_string(), + } +} /// Helper function to set / unset cold storage slot of the target address. fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) { @@ -1582,20 +1544,3 @@ fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) storage_slot.is_cold = cold; } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_array_size() { - assert_eq!(extract_array_size("uint256[3]"), Some(3)); - assert_eq!(extract_array_size("address[10]"), Some(10)); - assert_eq!(extract_array_size("bool[5]"), Some(5)); - assert_eq!(extract_array_size("bytes32[100]"), Some(100)); - assert_eq!(extract_array_size("uint256"), None); - assert_eq!(extract_array_size("mapping(address => uint256)"), None); - assert_eq!(extract_array_size("uint256[]"), None); // Dynamic array - assert_eq!(extract_array_size("CustomStruct[42]"), Some(42)); - } -} From e78b0870e47fc90ff191362e636acf5b8d0a895a Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:17:53 +0530 Subject: [PATCH 22/41] feat: decode storage values --- crates/cheatcodes/src/evm.rs | 301 ++++++++++-------- .../cheats/StateDiffStorageLayout.t.sol | 39 ++- 2 files changed, 212 insertions(+), 128 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 8d28969a0418e..e3c55bacbe534 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -6,13 +6,13 @@ use crate::{ inspector::{Ecx, RecordDebugStepInfo}, }; use alloy_consensus::TxEnvelope; -use alloy_dyn_abi::DynSolType; +use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_genesis::{Genesis, GenesisAccount}; -use alloy_primitives::{Address, B256, U256, map::HashMap}; +use alloy_primitives::{Address, B256, U256, hex, map::HashMap}; use alloy_rlp::Decodable; use alloy_sol_types::SolValue; use foundry_common::fs::{read_json_file, write_json_file}; -use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType}; +use foundry_compilers::artifacts::StorageLayout; use foundry_evm_core::{ ContextExt, backend::{DatabaseExt, RevertStateSnapshotAction}, @@ -106,20 +106,50 @@ struct SlotStateDiff { previous_value: B256, /// Current storage value. new_value: B256, + /// Decoded Slot Values + decoded: Option, /// Slot Info #[serde(skip_serializing_if = "Option::is_none", flatten)] slot_info: Option, } -#[derive(Serialize, Default, Debug)] +#[derive(Serialize, Debug)] struct SlotInfo { label: String, - #[serde(rename = "type")] - storage_type: String, + #[serde(rename = "type", serialize_with = "serialize_dyn_sol_type")] + dyn_sol_type: DynSolType, offset: i64, slot: String, } +fn serialize_dyn_sol_type(dyn_type: &DynSolType, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&dyn_type.to_string()) +} + +#[derive(Debug)] +struct DecodedSlotValues { + /// Initial decoded storage value + previous_value: DynSolValue, + /// Current decoded storage value + new_value: DynSolValue, +} + +impl Serialize for DecodedSlotValues { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?; + state.serialize_field("previousValue", &format_dyn_sol_value_raw(&self.previous_value))?; + state.serialize_field("newValue", &format_dyn_sol_value_raw(&self.new_value))?; + state.end() + } +} + /// Balance diff info. #[derive(Serialize, Default)] #[serde(rename_all = "camelCase")] @@ -167,22 +197,39 @@ impl Display for AccountStateDiffs { if !&self.state_diff.is_empty() { writeln!(f, "- state diff:")?; for (slot, slot_changes) in &self.state_diff { - // If we have storage layout info, include it - if let Some(slot_info) = &slot_changes.slot_info { - writeln!( - f, - "@ {slot} ({}, {}): {} → {}", - slot_info.label, - slot_info.storage_type, - slot_changes.previous_value, - slot_changes.new_value - )?; - } else { - writeln!( - f, - "@ {slot}: {} → {}", - slot_changes.previous_value, slot_changes.new_value - )?; + match (&slot_changes.slot_info, &slot_changes.decoded) { + (Some(slot_info), Some(decoded)) => { + // Have both slot info and decoded values + writeln!( + f, + "@ {slot} ({}, {}): {} → {} [decoded: {} → {}]", + slot_info.label, + slot_info.dyn_sol_type, + slot_changes.previous_value, + slot_changes.new_value, + format_dyn_sol_value_raw(&decoded.previous_value), + format_dyn_sol_value_raw(&decoded.new_value) + )?; + } + (Some(slot_info), None) => { + // Have slot info but no decoded values + writeln!( + f, + "@ {slot} ({}, {}): {} → {}", + slot_info.label, + slot_info.dyn_sol_type, + slot_changes.previous_value, + slot_changes.new_value + )?; + } + _ => { + // No slot info + writeln!( + f, + "@ {slot}: {} → {}", + slot_changes.previous_value, slot_changes.new_value + )?; + } } } } @@ -1312,26 +1359,44 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap { - // Get storage layout info for this slot if available + // Get storage layout info for this slot let slot_info = storage_layouts .get(&storage_access.account) - .and_then(|layout| { - tracing::debug!( - "Looking for slot {} in layout for account {}", - storage_access.slot, - storage_access.account - ); - get_slot_info(layout, &storage_access.slot) - }); + .and_then(|layout| get_slot_info(layout, &storage_access.slot)); + + // Try to decode values if we have slot info + let decoded = slot_info.as_ref().and_then(|info| { + let prev = decode_storage_value( + storage_access.previousValue, + &info.dyn_sol_type, + )?; + let new = decode_storage_value( + storage_access.newValue, + &info.dyn_sol_type, + )?; + Some(DecodedSlotValues { previous_value: prev, new_value: new }) + }); slot_state_diff.insert(SlotStateDiff { previous_value: storage_access.previousValue, new_value: storage_access.newValue, + decoded, slot_info, }); } Entry::Occupied(mut slot_state_diff) => { - slot_state_diff.get_mut().new_value = storage_access.newValue; + let entry = slot_state_diff.get_mut(); + entry.new_value = storage_access.newValue; + + if let Some(slot_info) = &entry.slot_info + && let Some(ref mut decoded) = entry.decoded + && let Some(new_value) = decode_storage_value( + storage_access.newValue, + &slot_info.dyn_sol_type, + ) + { + decoded.new_value = new_value; + } } } } @@ -1396,106 +1461,52 @@ fn get_contract_data<'a>( artifacts.find_by_deployed_code(&code_bytes) } -/// Helper function to check if a storage entry matches the given slot exactly. -fn check_exact_slot_match( - storage: &Storage, - storage_layout: &StorageLayout, - slot_str: &str, -) -> Option { - trace!(type_label = %storage.storage_type, "checking type"); - if storage.slot == slot_str { - // Get the StorageType which contains the clean label - let storage_type = storage_layout.types.get(&storage.storage_type)?; - let type_label = storage_type.label.clone(); - - // Parse the type to check if it's an array - let label = if let Ok(dyn_type) = DynSolType::parse(&type_label) { - if let DynSolType::FixedArray(_, _) = &dyn_type { - // For arrays, label the base slot with indices - let indices = get_array_base_indices(&dyn_type); - format!("{}{}", storage.label, indices) - } else { - storage.label.clone() - } - } else { - storage.label.clone() - }; - - return Some(SlotInfo { - label, - storage_type: type_label, - offset: storage.offset, - slot: storage.slot.clone(), - }); - } - None -} - -/// Helper function to check if a slot is part of a static array. -fn check_array_slot_match( - storage: &Storage, - type_info: &StorageType, - base_slot: U256, - slot: U256, -) -> Option { - // StorageType.label already contains the clean type string without 't_' prefix - let type_label = type_info.label.clone(); - - // Parse the type using DynSolType - trace!(type_label = %type_label, "checking if type is a static_array"); - let dyn_type = DynSolType::parse(&type_label).ok()?; - - // Check if it's a static array - if !matches!(dyn_type, DynSolType::FixedArray(_, _)) { - return None; - } - - // For arrays, calculate the number of slots based on the actual storage size - let Ok(total_bytes) = type_info.number_of_bytes.parse::() else { - return None; - }; - - // Each slot is 32 bytes - let total_slots = total_bytes.div_ceil(32); - - // Check if slot is within array range - if slot <= base_slot || slot >= base_slot + U256::from(total_slots) { - return None; - } - - let index = slot - base_slot; - let index_u64 = index.to::(); - - // Generate the label with the correct index - let label = format_array_element_label(&storage.label, &dyn_type, index_u64); - - Some(SlotInfo { label, storage_type: type_label, offset: 0, slot: slot.to_string() }) -} - -/// Helper function to get storage layout info for a specific slot. +/// Gets storage layout info for a specific slot. fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { - // Convert B256 slot to decimal string for comparison with storage layout - // Storage layout stores slots as decimal strings like "0", "1", "2" let slot = U256::from_be_bytes(slot.0); let slot_str = slot.to_string(); - // Single loop to check both exact matches and array ranges for storage in &storage_layout.storage { - // Parse base slot as U256 to handle large slot numbers - let Ok(base_slot) = U256::from_str(&storage.slot) else { - continue; - }; - - // First check for exact match - if let Some(slot_info) = check_exact_slot_match(storage, storage_layout, &slot_str) { - return Some(slot_info); + let base_slot = U256::from_str(&storage.slot).ok()?; + let storage_type = storage_layout.types.get(&storage.storage_type)?; + let dyn_type = DynSolType::parse(&storage_type.label).ok()?; + + // Check for exact slot match + if storage.slot == slot_str { + let label = match &dyn_type { + DynSolType::FixedArray(_, _) => { + // For arrays, label the base slot with indices + format!("{}{}", storage.label, get_array_base_indices(&dyn_type)) + } + _ => storage.label.clone(), + }; + + return Some(SlotInfo { + label, + dyn_sol_type: dyn_type, + offset: storage.offset, + slot: storage.slot.clone(), + }); } - // Check if this is a static array with inplace encoding (contiguous storage) - if let Some(type_info) = storage_layout.types.get(&storage.storage_type) - && let Some(slot_info) = check_array_slot_match(storage, type_info, base_slot, slot) + // Check if slot is part of a static array + if let DynSolType::FixedArray(_, _) = &dyn_type + && let Ok(total_bytes) = storage_type.number_of_bytes.parse::() { - return Some(slot_info); + let total_slots = total_bytes.div_ceil(32); + + // Check if slot is within array range + if slot > base_slot && slot < base_slot + U256::from(total_slots) { + let index = (slot - base_slot).to::(); + let label = format_array_element_label(&storage.label, &dyn_type, index); + + return Some(SlotInfo { + label, + dyn_sol_type: dyn_type, + offset: 0, + slot: slot.to_string(), + }); + } } } @@ -1521,7 +1532,7 @@ fn get_array_base_indices(dyn_type: &DynSolType) -> String { /// Helper function to format an array element label given its index fn format_array_element_label(base_label: &str, dyn_type: &DynSolType, index: u64) -> String { match dyn_type { - DynSolType::FixedArray(inner, _size) => { + DynSolType::FixedArray(inner, _) => { if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() { // 2D array: calculate row and column let row = index / (*inner_size as u64); @@ -1536,6 +1547,46 @@ fn format_array_element_label(base_label: &str, dyn_type: &DynSolType, index: u6 } } +/// Helper function to decode a single storage value using its DynSolType +fn decode_storage_value(value: B256, dyn_type: &DynSolType) -> Option { + // Storage values are always 32 bytes, stored as a single word + // For arrays, we need to unwrap to the base element type + let mut actual_type = dyn_type; + + // Unwrap nested arrays to get to the base element type + while let DynSolType::FixedArray(elem_type, _) = actual_type { + actual_type = elem_type.as_ref(); + } + + // Use abi_decode to decode the value + actual_type.abi_decode(&value.0).ok() +} + +/// Helper function to format DynSolValue as raw string without type information +fn format_dyn_sol_value_raw(value: &DynSolValue) -> String { + match value { + DynSolValue::Bool(b) => b.to_string(), + DynSolValue::Int(i, _) => i.to_string(), + DynSolValue::Uint(u, _) => u.to_string(), + DynSolValue::FixedBytes(bytes, size) => hex::encode_prefixed(&bytes.0[..*size]), + DynSolValue::Address(addr) => addr.to_string(), + DynSolValue::Function(func) => func.as_address_and_selector().1.to_string(), + DynSolValue::Bytes(bytes) => hex::encode_prefixed(bytes), + DynSolValue::String(s) => s.clone(), + DynSolValue::Array(values) | DynSolValue::FixedArray(values) => { + let formatted: Vec = values.iter().map(format_dyn_sol_value_raw).collect(); + format!("[{}]", formatted.join(", ")) + } + DynSolValue::Tuple(values) => { + let formatted: Vec = values.iter().map(format_dyn_sol_value_raw).collect(); + format!("({})", formatted.join(", ")) + } + DynSolValue::CustomStruct { name: _, prop_names: _, tuple } => { + format_dyn_sol_value_raw(&DynSolValue::Tuple(tuple.clone())) + } + } +} + /// Helper function to set / unset cold storage slot of the target address. fn set_cold_slot(ccx: &mut CheatsCtxt, target: Address, slot: U256, cold: bool) { if let Some(account) = ccx.ecx.journaled_state.state.get_mut(&target) diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 77d40bb8bfa69..9ae866117be89 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -114,9 +114,9 @@ contract StateDiffStorageLayoutTest is DSTest { vm.startStateDiffRecording(); // Modify storage slots with known positions - simpleStorage.setValue(42); // Modifies slot 0 - simpleStorage.setOwner(address(this)); // Modifies slot 1 - simpleStorage.setValues(100, 200, 300); // Modifies slots 2, 3, 4 + simpleStorage.setValue(42); // Modifies slot 0 (value) + simpleStorage.setOwner(address(this)); // Modifies slot 1 (owner) + simpleStorage.setValues(100, 200, 300); // Modifies slots 2, 3, 4 (values array) // Get the state diff as string string memory stateDiff = vm.getStateDiff(); @@ -136,6 +136,20 @@ contract StateDiffStorageLayoutTest is DSTest { assertContains(stateDiffJson, "\"type\":\"address\"", "Should contain address type"); assertContains(stateDiffJson, "\"type\":\"uint256[3]\"", "Should contain uint256[3] type"); + // Check for decoded values + assertContains(stateDiffJson, "\"decoded\":", "Should contain decoded values"); + + // Check specific decoded values within the decoded object + // The value 42 should be decoded in the first slot + assertContains( + stateDiffJson, "\"decoded\":{\"previousValue\":\"0\",\"newValue\":\"42\"}", "Should decode value 42" + ); + + // Check that array values are decoded properly (they will have separate decoded objects) + assertContains(stateDiffJson, "\"newValue\":\"100\"", "Should decode array value 100"); + assertContains(stateDiffJson, "\"newValue\":\"200\"", "Should decode array value 200"); + assertContains(stateDiffJson, "\"newValue\":\"300\"", "Should decode array value 300"); + // Stop recording and verify we get the expected account accesses Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); assertTrue(accesses.length >= 3, "Should have at least 3 account accesses for the calls"); @@ -199,6 +213,16 @@ contract StateDiffStorageLayoutTest is DSTest { assertContains(stateDiffJson, "\"type\":\"bool[5]\"", "Should contain bool[5] type"); assertContains(stateDiffJson, "\"type\":\"bytes32[2]\"", "Should contain bytes32[2] type"); + // Check decoded values + assertContains(stateDiffJson, "\"decoded\":", "Should contain decoded values"); + // Check addresses are decoded as raw hex strings + assertContains( + stateDiffJson, "\"newValue\":\"0x0000000000000000000000000000000000000001\"", "Should decode address 1" + ); + assertContains( + stateDiffJson, "\"newValue\":\"0x0000000000000000000000000000000000000002\"", "Should decode address 2" + ); + // Stop recording and verify account accesses Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); assertTrue(accesses.length > 0, "Should have account accesses"); @@ -250,6 +274,15 @@ contract StateDiffStorageLayoutTest is DSTest { // Check that we have the right type assertContains(stateDiffJson, "\"type\":\"uint256[3][2]\"", "Should contain 2D array type"); + // Check decoded values for 2D arrays + assertContains(stateDiffJson, "\"decoded\":", "Should contain decoded values"); + assertContains(stateDiffJson, "\"newValue\":\"100\"", "Should decode matrix[0][0] = 100"); + assertContains(stateDiffJson, "\"newValue\":\"101\"", "Should decode matrix[0][1] = 101"); + assertContains(stateDiffJson, "\"newValue\":\"102\"", "Should decode matrix[0][2] = 102"); + assertContains(stateDiffJson, "\"newValue\":\"200\"", "Should decode matrix[1][0] = 200"); + assertContains(stateDiffJson, "\"newValue\":\"201\"", "Should decode matrix[1][1] = 201"); + assertContains(stateDiffJson, "\"newValue\":\"202\"", "Should decode matrix[1][2] = 202"); + vm.stopAndReturnStateDiff(); } From ebcef043bac9c6f80e1bc5f563e45520bebcd9a2 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:32:55 +0530 Subject: [PATCH 23/41] doc nit --- crates/cheatcodes/src/evm.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index e3c55bacbe534..f4c0528414cc0 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1513,7 +1513,6 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option String { match dyn_type { DynSolType::FixedArray(inner, _) => { From 65db931a361899a534a5b70e2d38e42a727ff9cd Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:33:32 +0530 Subject: [PATCH 24/41] skip decoded serialization if none --- crates/cheatcodes/src/evm.rs | 1 + testdata/foundry.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index f4c0528414cc0..2731522070581 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -107,6 +107,7 @@ struct SlotStateDiff { /// Current storage value. new_value: B256, /// Decoded Slot Values + #[serde(skip_serializing_if = "Option::is_none")] decoded: Option, /// Slot Info #[serde(skip_serializing_if = "Option::is_none", flatten)] diff --git a/testdata/foundry.toml b/testdata/foundry.toml index e9189bb008a32..ac57bd40f86a6 100644 --- a/testdata/foundry.toml +++ b/testdata/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc = "0.8.18" +# solc = "0.8.18" block_base_fee_per_gas = 0 block_coinbase = "0x0000000000000000000000000000000000000000" block_difficulty = 0 @@ -10,7 +10,7 @@ bytecode_hash = "ipfs" cache = true cache_path = "cache" evm_version = "paris" -extra_output = [] +extra_output = ["storageLayout"] extra_output_files = [] ffi = false force = false From 7d3c87be70b6ed7b78702530ac755620c0f1b00e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:33:44 +0530 Subject: [PATCH 25/41] nit --- testdata/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/foundry.toml b/testdata/foundry.toml index ac57bd40f86a6..ea23af5935fb0 100644 --- a/testdata/foundry.toml +++ b/testdata/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -# solc = "0.8.18" +solc = "0.8.18" block_base_fee_per_gas = 0 block_coinbase = "0x0000000000000000000000000000000000000000" block_difficulty = 0 From e63e817d9f39fb777082ca3c581cda2265f7db73 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:34:44 +0530 Subject: [PATCH 26/41] fmt --- testdata/default/cheats/RecordAccountAccesses.t.sol | 5 ++++- testdata/default/repros/Issue9643.t.sol | 8 ++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/testdata/default/cheats/RecordAccountAccesses.t.sol b/testdata/default/cheats/RecordAccountAccesses.t.sol index 0a5456f435df3..6288785c61a09 100644 --- a/testdata/default/cheats/RecordAccountAccesses.t.sol +++ b/testdata/default/cheats/RecordAccountAccesses.t.sol @@ -349,7 +349,10 @@ contract RecordAccountAccessesTest is DSTest { expectedStateDiff = string.concat(expectedStateDiff, callerAddress); expectedStateDiff = string.concat(expectedStateDiff, "\ncontract: default/cheats/RecordAccountAccesses.t.sol:SelfCaller"); - expectedStateDiff = string.concat(expectedStateDiff, "\n- balance diff: 0 \xE2\x86\x92 2000000000000000000\n- nonce diff: 0 \xE2\x86\x92 1\n\n"); + expectedStateDiff = string.concat( + expectedStateDiff, + "\n- balance diff: 0 \xE2\x86\x92 2000000000000000000\n- nonce diff: 0 \xE2\x86\x92 1\n\n" + ); assertEq(expectedStateDiff, cheats.getStateDiff()); Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff()); diff --git a/testdata/default/repros/Issue9643.t.sol b/testdata/default/repros/Issue9643.t.sol index 68191952270d0..e3c2cb1bd9cb6 100644 --- a/testdata/default/repros/Issue9643.t.sol +++ b/testdata/default/repros/Issue9643.t.sol @@ -27,12 +27,8 @@ contract DelegateProxy { let result := delegatecall(gas(), addr, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } } } } From f9ffcea3918edb5ff5ef46f7ed7768a55ecf3d95 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:40:14 +0530 Subject: [PATCH 27/41] fix --- crates/cheatcodes/src/evm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 50ca0d0bf6f43..91296d9f5f8fb 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1288,7 +1288,7 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap Date: Wed, 13 Aug 2025 14:57:55 +0530 Subject: [PATCH 28/41] fix --- testdata/default/repros/Issue9643.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/default/repros/Issue9643.t.sol b/testdata/default/repros/Issue9643.t.sol index e3c2cb1bd9cb6..58f9f92c49914 100644 --- a/testdata/default/repros/Issue9643.t.sol +++ b/testdata/default/repros/Issue9643.t.sol @@ -42,7 +42,7 @@ contract Issue9643Test is DSTest { proxied.setCounter(42); string memory rawDiff = vm.getStateDiffJson(); assertEq( - '{"0x2e234dae75c793f67a35089c9d99245e1c58470b":{"label":null,"contract":"default/repros/Issue9643.t.sol:DelegateProxy","balanceDiff":null,"nonceDiff":{"previousValue":0,"newValue":1},"stateDiff":{"0x0000000000000000000000000000000000000000000000000000000000000000":{"previousValue":"0x0000000000000000000000000000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000000000000000000000000000002a"}}},"0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f":{"label":null,"balanceDiff":null,"nonceDiff":{"previousValue":0,"newValue":1},"stateDiff":{}}}', + '{"0x2e234dae75c793f67a35089c9d99245e1c58470b":{"label":null,"contract":null,"balanceDiff":null,"nonceDiff":{"previousValue":0,"newValue":1},"stateDiff":{"0x0000000000000000000000000000000000000000000000000000000000000000":{"previousValue":"0x0000000000000000000000000000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000000000000000000000000000002a"}}},"0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f":{"label":null,"contract":null,"balanceDiff":null,"nonceDiff":{"previousValue":0,"newValue":1},"stateDiff":{}}}', rawDiff ); } From 055797d6517ed4b5b40810506def5fd2537678ec Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:02:14 +0530 Subject: [PATCH 29/41] fix --- crates/cheatcodes/src/evm.rs | 1 + testdata/default/repros/Issue9643.t.sol | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 91296d9f5f8fb..e9bd8769d6871 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1289,6 +1289,7 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap Date: Mon, 18 Aug 2025 18:03:08 +0530 Subject: [PATCH 30/41] fix --- crates/cheatcodes/src/evm.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index d4802b1f2de40..07ff5399793ac 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1568,6 +1568,7 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option String { match dyn_type { DynSolType::FixedArray(inner, _) => { @@ -1605,15 +1606,11 @@ fn format_array_element_label(base_label: &str, dyn_type: &DynSolType, index: u6 fn decode_storage_value(value: B256, dyn_type: &DynSolType) -> Option { // Storage values are always 32 bytes, stored as a single word // For arrays, we need to unwrap to the base element type - let mut actual_type = dyn_type; - - // Unwrap nested arrays to get to the base element type - while let DynSolType::FixedArray(elem_type, _) = actual_type { - actual_type = elem_type.as_ref(); + if let DynSolType::FixedArray(elem_type, _) = dyn_type { + elem_type.as_ref().abi_decode(&value.0).ok() + } else { + dyn_type.abi_decode(&value.0).ok() } - - // Use abi_decode to decode the value - actual_type.abi_decode(&value.0).ok() } /// Helper function to format DynSolValue as raw string without type information From 927e75613cdbcdf17fa1682e25e1a94ef6acf54d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:40:35 +0530 Subject: [PATCH 31/41] while decode --- crates/cheatcodes/src/evm.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 07ff5399793ac..99f5b21a4f519 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1606,11 +1606,14 @@ fn format_array_element_label(base_label: &str, dyn_type: &DynSolType, index: u6 fn decode_storage_value(value: B256, dyn_type: &DynSolType) -> Option { // Storage values are always 32 bytes, stored as a single word // For arrays, we need to unwrap to the base element type - if let DynSolType::FixedArray(elem_type, _) = dyn_type { - elem_type.as_ref().abi_decode(&value.0).ok() - } else { - dyn_type.abi_decode(&value.0).ok() + let mut actual_type = dyn_type; + // Unwrap nested arrays to get to the base element type. + while let DynSolType::FixedArray(elem_type, _) = actual_type { + actual_type = elem_type.as_ref(); } + + // Use abi_decode to decode the value + actual_type.abi_decode(&value.0).ok() } /// Helper function to format DynSolValue as raw string without type information From e487e82e44f533cb06a6cce2d41a0e0ce56e5b0f Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:59:53 +0530 Subject: [PATCH 32/41] fix: show only decoded in plaintext / display output + test --- crates/cheatcodes/src/evm.rs | 10 +- .../cheats/StateDiffStorageLayout.t.sol | 146 +++++++++++------- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 99f5b21a4f519..1eae391b23cff 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -218,20 +218,18 @@ impl Display for AccountStateDiffs { for (slot, slot_changes) in &self.state_diff { match (&slot_changes.slot_info, &slot_changes.decoded) { (Some(slot_info), Some(decoded)) => { - // Have both slot info and decoded values + // Have both slot info and decoded values - only show decoded values writeln!( f, - "@ {slot} ({}, {}): {} → {} [decoded: {} → {}]", + "@ {slot} ({}, {}): {} → {}", slot_info.label, slot_info.dyn_sol_type, - slot_changes.previous_value, - slot_changes.new_value, format_dyn_sol_value_raw(&decoded.previous_value), format_dyn_sol_value_raw(&decoded.new_value) )?; } (Some(slot_info), None) => { - // Have slot info but no decoded values + // Have slot info but no decoded values - show raw hex values writeln!( f, "@ {slot} ({}, {}): {} → {}", @@ -242,7 +240,7 @@ impl Display for AccountStateDiffs { )?; } _ => { - // No slot info + // No slot info - show raw hex values writeln!( f, "@ {slot}: {} → {}", diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 9ae866117be89..961a2cd4b4363 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -126,29 +126,27 @@ contract StateDiffStorageLayoutTest is DSTest { // The JSON should contain storage layout info for all slots // We check the JSON contains expected substrings for the labels and types - assertContains(stateDiffJson, "\"label\":\"value\"", "Should contain 'value' label"); - assertContains(stateDiffJson, "\"label\":\"owner\"", "Should contain 'owner' label"); - assertContains(stateDiffJson, "\"label\":\"values[0]\"", "Should contain 'values[0]' label"); - assertContains(stateDiffJson, "\"label\":\"values[1]\"", "Should contain 'values[1]' label"); - assertContains(stateDiffJson, "\"label\":\"values[2]\"", "Should contain 'values[2]' label"); + assertContains(stateDiffJson, '"label":"value"', "Should contain 'value' label"); + assertContains(stateDiffJson, '"label":"owner"', "Should contain 'owner' label"); + assertContains(stateDiffJson, '"label":"values[0]"', "Should contain 'values[0]' label"); + assertContains(stateDiffJson, '"label":"values[1]"', "Should contain 'values[1]' label"); + assertContains(stateDiffJson, '"label":"values[2]"', "Should contain 'values[2]' label"); - assertContains(stateDiffJson, "\"type\":\"uint256\"", "Should contain uint256 type"); - assertContains(stateDiffJson, "\"type\":\"address\"", "Should contain address type"); - assertContains(stateDiffJson, "\"type\":\"uint256[3]\"", "Should contain uint256[3] type"); + assertContains(stateDiffJson, '"type":"uint256"', "Should contain uint256 type"); + assertContains(stateDiffJson, '"type":"address"', "Should contain address type"); + assertContains(stateDiffJson, '"type":"uint256[3]"', "Should contain uint256[3] type"); // Check for decoded values - assertContains(stateDiffJson, "\"decoded\":", "Should contain decoded values"); + assertContains(stateDiffJson, '"decoded":', "Should contain decoded values"); // Check specific decoded values within the decoded object // The value 42 should be decoded in the first slot - assertContains( - stateDiffJson, "\"decoded\":{\"previousValue\":\"0\",\"newValue\":\"42\"}", "Should decode value 42" - ); + assertContains(stateDiffJson, '"decoded":{"previousValue":"0","newValue":"42"}', "Should decode value 42"); // Check that array values are decoded properly (they will have separate decoded objects) - assertContains(stateDiffJson, "\"newValue\":\"100\"", "Should decode array value 100"); - assertContains(stateDiffJson, "\"newValue\":\"200\"", "Should decode array value 200"); - assertContains(stateDiffJson, "\"newValue\":\"300\"", "Should decode array value 300"); + assertContains(stateDiffJson, '"newValue":"100"', "Should decode array value 100"); + assertContains(stateDiffJson, '"newValue":"200"', "Should decode array value 200"); + assertContains(stateDiffJson, '"newValue":"300"', "Should decode array value 300"); // Stop recording and verify we get the expected account accesses Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); @@ -195,32 +193,32 @@ contract StateDiffStorageLayoutTest is DSTest { string memory stateDiffJson = vm.getStateDiffJson(); // Verify all array types are properly labeled with indices - assertContains(stateDiffJson, "\"label\":\"numbers[0]\"", "Should contain 'numbers[0]' label"); - assertContains(stateDiffJson, "\"label\":\"numbers[1]\"", "Should contain 'numbers[1]' label"); - assertContains(stateDiffJson, "\"label\":\"numbers[2]\"", "Should contain 'numbers[2]' label"); + assertContains(stateDiffJson, '"label":"numbers[0]"', "Should contain 'numbers[0]' label"); + assertContains(stateDiffJson, '"label":"numbers[1]"', "Should contain 'numbers[1]' label"); + assertContains(stateDiffJson, '"label":"numbers[2]"', "Should contain 'numbers[2]' label"); - assertContains(stateDiffJson, "\"label\":\"addresses[0]\"", "Should contain 'addresses[0]' label"); - assertContains(stateDiffJson, "\"label\":\"addresses[1]\"", "Should contain 'addresses[1]' label"); + assertContains(stateDiffJson, '"label":"addresses[0]"', "Should contain 'addresses[0]' label"); + assertContains(stateDiffJson, '"label":"addresses[1]"', "Should contain 'addresses[1]' label"); - assertContains(stateDiffJson, "\"label\":\"flags[0]\"", "Should contain 'flags[0]' label"); + assertContains(stateDiffJson, '"label":"flags[0]"', "Should contain 'flags[0]' label"); - assertContains(stateDiffJson, "\"label\":\"hashes[0]\"", "Should contain 'hashes[0]' label"); - assertContains(stateDiffJson, "\"label\":\"hashes[1]\"", "Should contain 'hashes[1]' label"); + assertContains(stateDiffJson, '"label":"hashes[0]"', "Should contain 'hashes[0]' label"); + assertContains(stateDiffJson, '"label":"hashes[1]"', "Should contain 'hashes[1]' label"); // Verify types are correctly identified - assertContains(stateDiffJson, "\"type\":\"uint256[3]\"", "Should contain uint256[3] type"); - assertContains(stateDiffJson, "\"type\":\"address[2]\"", "Should contain address[2] type"); - assertContains(stateDiffJson, "\"type\":\"bool[5]\"", "Should contain bool[5] type"); - assertContains(stateDiffJson, "\"type\":\"bytes32[2]\"", "Should contain bytes32[2] type"); + assertContains(stateDiffJson, '"type":"uint256[3]"', "Should contain uint256[3] type"); + assertContains(stateDiffJson, '"type":"address[2]"', "Should contain address[2] type"); + assertContains(stateDiffJson, '"type":"bool[5]"', "Should contain bool[5] type"); + assertContains(stateDiffJson, '"type":"bytes32[2]"', "Should contain bytes32[2] type"); // Check decoded values - assertContains(stateDiffJson, "\"decoded\":", "Should contain decoded values"); + assertContains(stateDiffJson, '"decoded":', "Should contain decoded values"); // Check addresses are decoded as raw hex strings assertContains( - stateDiffJson, "\"newValue\":\"0x0000000000000000000000000000000000000001\"", "Should decode address 1" + stateDiffJson, '"newValue":"0x0000000000000000000000000000000000000001"', "Should decode address 1" ); assertContains( - stateDiffJson, "\"newValue\":\"0x0000000000000000000000000000000000000002\"", "Should decode address 2" + stateDiffJson, '"newValue":"0x0000000000000000000000000000000000000002"', "Should decode address 2" ); // Stop recording and verify account accesses @@ -239,12 +237,12 @@ contract StateDiffStorageLayoutTest is DSTest { string memory stateDiffJson = vm.getStateDiffJson(); // Check JSON structure contains expected fields - assertContains(stateDiffJson, "\"previousValue\":", "JSON should contain previousValue field"); - assertContains(stateDiffJson, "\"newValue\":", "JSON should contain newValue field"); - assertContains(stateDiffJson, "\"label\":", "JSON should contain label field"); - assertContains(stateDiffJson, "\"type\":", "JSON should contain type field"); - assertContains(stateDiffJson, "\"offset\":", "JSON should contain offset field"); - assertContains(stateDiffJson, "\"slot\":", "JSON should contain slot field"); + assertContains(stateDiffJson, '"previousValue":', "JSON should contain previousValue field"); + assertContains(stateDiffJson, '"newValue":', "JSON should contain newValue field"); + assertContains(stateDiffJson, '"label":', "JSON should contain label field"); + assertContains(stateDiffJson, '"type":', "JSON should contain type field"); + assertContains(stateDiffJson, '"offset":', "JSON should contain offset field"); + assertContains(stateDiffJson, '"slot":', "JSON should contain slot field"); vm.stopAndReturnStateDiff(); } @@ -264,24 +262,24 @@ contract StateDiffStorageLayoutTest is DSTest { string memory stateDiffJson = vm.getStateDiffJson(); // Verify the labels for 2D array elements - assertContains(stateDiffJson, "\"label\":\"matrix[0][0]\"", "Should contain matrix[0][0] label"); - assertContains(stateDiffJson, "\"label\":\"matrix[0][1]\"", "Should contain matrix[0][1] label"); - assertContains(stateDiffJson, "\"label\":\"matrix[0][2]\"", "Should contain matrix[0][2] label"); - assertContains(stateDiffJson, "\"label\":\"matrix[1][0]\"", "Should contain matrix[1][0] label"); - assertContains(stateDiffJson, "\"label\":\"matrix[1][1]\"", "Should contain matrix[1][1] label"); - assertContains(stateDiffJson, "\"label\":\"matrix[1][2]\"", "Should contain matrix[1][2] label"); + assertContains(stateDiffJson, '"label":"matrix[0][0]"', "Should contain matrix[0][0] label"); + assertContains(stateDiffJson, '"label":"matrix[0][1]"', "Should contain matrix[0][1] label"); + assertContains(stateDiffJson, '"label":"matrix[0][2]"', "Should contain matrix[0][2] label"); + assertContains(stateDiffJson, '"label":"matrix[1][0]"', "Should contain matrix[1][0] label"); + assertContains(stateDiffJson, '"label":"matrix[1][1]"', "Should contain matrix[1][1] label"); + assertContains(stateDiffJson, '"label":"matrix[1][2]"', "Should contain matrix[1][2] label"); // Check that we have the right type - assertContains(stateDiffJson, "\"type\":\"uint256[3][2]\"", "Should contain 2D array type"); + assertContains(stateDiffJson, '"type":"uint256[3][2]"', "Should contain 2D array type"); // Check decoded values for 2D arrays - assertContains(stateDiffJson, "\"decoded\":", "Should contain decoded values"); - assertContains(stateDiffJson, "\"newValue\":\"100\"", "Should decode matrix[0][0] = 100"); - assertContains(stateDiffJson, "\"newValue\":\"101\"", "Should decode matrix[0][1] = 101"); - assertContains(stateDiffJson, "\"newValue\":\"102\"", "Should decode matrix[0][2] = 102"); - assertContains(stateDiffJson, "\"newValue\":\"200\"", "Should decode matrix[1][0] = 200"); - assertContains(stateDiffJson, "\"newValue\":\"201\"", "Should decode matrix[1][1] = 201"); - assertContains(stateDiffJson, "\"newValue\":\"202\"", "Should decode matrix[1][2] = 202"); + assertContains(stateDiffJson, '"decoded":', "Should contain decoded values"); + assertContains(stateDiffJson, '"newValue":"100"', "Should decode matrix[0][0] = 100"); + assertContains(stateDiffJson, '"newValue":"101"', "Should decode matrix[0][1] = 101"); + assertContains(stateDiffJson, '"newValue":"102"', "Should decode matrix[0][2] = 102"); + assertContains(stateDiffJson, '"newValue":"200"', "Should decode matrix[1][0] = 200"); + assertContains(stateDiffJson, '"newValue":"201"', "Should decode matrix[1][1] = 201"); + assertContains(stateDiffJson, '"newValue":"202"', "Should decode matrix[1][2] = 202"); vm.stopAndReturnStateDiff(); } @@ -304,20 +302,48 @@ contract StateDiffStorageLayoutTest is DSTest { string memory stateDiffJson = vm.getStateDiffJson(); // Check for proper types - assertContains(stateDiffJson, "\"type\":\"address[2][3]\"", "Should contain address 2D array type"); - assertContains(stateDiffJson, "\"type\":\"bytes32[2][4]\"", "Should contain bytes32 2D array type"); + assertContains(stateDiffJson, '"type":"address[2][3]"', "Should contain address 2D array type"); + assertContains(stateDiffJson, '"type":"bytes32[2][4]"', "Should contain bytes32 2D array type"); // Verify address 2D array labels - assertContains(stateDiffJson, "\"label\":\"addresses2D[0][0]\"", "Should contain addresses2D[0][0] label"); - assertContains(stateDiffJson, "\"label\":\"addresses2D[0][1]\"", "Should contain addresses2D[0][1] label"); - assertContains(stateDiffJson, "\"label\":\"addresses2D[1][0]\"", "Should contain addresses2D[1][0] label"); - assertContains(stateDiffJson, "\"label\":\"addresses2D[2][1]\"", "Should contain addresses2D[2][1] label"); + assertContains(stateDiffJson, '"label":"addresses2D[0][0]"', "Should contain addresses2D[0][0] label"); + assertContains(stateDiffJson, '"label":"addresses2D[0][1]"', "Should contain addresses2D[0][1] label"); + assertContains(stateDiffJson, '"label":"addresses2D[1][0]"', "Should contain addresses2D[1][0] label"); + assertContains(stateDiffJson, '"label":"addresses2D[2][1]"', "Should contain addresses2D[2][1] label"); // Verify data 2D array labels - assertContains(stateDiffJson, "\"label\":\"data2D[0][0]\"", "Should contain data2D[0][0] label"); - assertContains(stateDiffJson, "\"label\":\"data2D[0][1]\"", "Should contain data2D[0][1] label"); - assertContains(stateDiffJson, "\"label\":\"data2D[1][0]\"", "Should contain data2D[1][0] label"); - assertContains(stateDiffJson, "\"label\":\"data2D[1][1]\"", "Should contain data2D[1][1] label"); + assertContains(stateDiffJson, '"label":"data2D[0][0]"', "Should contain data2D[0][0] label"); + assertContains(stateDiffJson, '"label":"data2D[0][1]"', "Should contain data2D[0][1] label"); + assertContains(stateDiffJson, '"label":"data2D[1][0]"', "Should contain data2D[1][0] label"); + assertContains(stateDiffJson, '"label":"data2D[1][1]"', "Should contain data2D[1][1] label"); + + vm.stopAndReturnStateDiff(); + } + + function testStateDiffDecodedValues() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Make changes to create state diffs with decoded values + simpleStorage.setValue(42); // uint256 value + simpleStorage.setOwner(address(0xBEEF)); // address value + simpleStorage.setValues(100, 200, 300); // array values + + // Get the state diff as string (not JSON) + string memory stateDiff = vm.getStateDiff(); + + // Test that decoded values are shown in the string format + // The output uses Unicode arrow → + // For uint256 values, should show decoded value "42" + assertContains(stateDiff, unicode"0 → 42", "Should show decoded uint256 value"); + + // For addresses, should show decoded address format + assertContains(stateDiff, "0x000000000000000000000000000000000000bEEF", "Should show decoded address"); + + // For array elements, should show decoded values + assertContains(stateDiff, unicode"0 → 100", "Should show decoded array value 100"); + assertContains(stateDiff, unicode"0 → 200", "Should show decoded array value 200"); + assertContains(stateDiff, unicode"0 → 300", "Should show decoded array value 300"); vm.stopAndReturnStateDiff(); } From aa3602202027f4041a110fcb5873d4b89b1168e5 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:06:46 +0530 Subject: [PATCH 33/41] feat: format slots to only significant bits in vm.getStateDiff output --- crates/cheatcodes/src/evm.rs | 26 ++++++ .../cheats/StateDiffStorageLayout.t.sol | 80 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 1eae391b23cff..cae2115cda327 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -216,6 +216,7 @@ impl Display for AccountStateDiffs { if !&self.state_diff.is_empty() { writeln!(f, "- state diff:")?; for (slot, slot_changes) in &self.state_diff { + let slot = format_slot(slot); match (&slot_changes.slot_info, &slot_changes.decoded) { (Some(slot_info), Some(decoded)) => { // Have both slot info and decoded values - only show decoded values @@ -1614,6 +1615,31 @@ fn decode_storage_value(value: B256, dyn_type: &DynSolType) -> Option String { + // Find the first non-zero byte + let first_non_zero = slot.0.iter().position(|&b| b != 0); + + match first_non_zero { + None => { + // All zeros - display as 0x00 + "0x00".to_string() + } + Some(pos) => { + // Format from the first non-zero byte onward + let significant_bytes = &slot.0[pos..]; + let hex_str = hex::encode(significant_bytes); + + // Ensure we always have an even number of hex digits (leading zero if odd) + if hex_str.len() % 2 == 1 { + format!("0x0{}", hex_str) + } else { + format!("0x{}", hex_str) + } + } + } +} + /// Helper function to format DynSolValue as raw string without type information fn format_dyn_sol_value_raw(value: &DynSolValue) -> String { match value { diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 961a2cd4b4363..54e381c8df4ae 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -97,16 +97,41 @@ contract TwoDArrayStorage { } } +contract LargeSlotStorage { + // Regular storage variables + uint256 public normalValue; // slot 0 + + function setLargeSlot(uint256 value) external { + assembly { + // Using a large slot number directly + sstore(0x123456789abcdef, value) + } + } + + function setMediumSlot(uint256 value) external { + assembly { + // Using a medium slot number (4096) + sstore(0x1000, value) + } + } + + function setNormalValue(uint256 value) external { + normalValue = value; + } +} + contract StateDiffStorageLayoutTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); SimpleStorage simpleStorage; VariousArrays variousArrays; TwoDArrayStorage twoDArrayStorage; + LargeSlotStorage largeSlotStorage; function setUp() public { simpleStorage = new SimpleStorage(); variousArrays = new VariousArrays(); twoDArrayStorage = new TwoDArrayStorage(); + largeSlotStorage = new LargeSlotStorage(); } function testSimpleStorageLayout() public { @@ -332,6 +357,13 @@ contract StateDiffStorageLayoutTest is DSTest { // Get the state diff as string (not JSON) string memory stateDiff = vm.getStateDiff(); + // Check that slots are formatted compactly (e.g., @ 0x00, @ 0x01, @ 0x02) + assertContains(stateDiff, "@ 0x00 (value", "Slot 0 should be formatted as 0x00"); + assertContains(stateDiff, "@ 0x01 (owner", "Slot 1 should be formatted as 0x01"); + assertContains(stateDiff, "@ 0x02 (values[0]", "Slot 2 should be formatted as 0x02"); + assertContains(stateDiff, "@ 0x03 (values[1]", "Slot 3 should be formatted as 0x03"); + assertContains(stateDiff, "@ 0x04 (values[2]", "Slot 4 should be formatted as 0x04"); + // Test that decoded values are shown in the string format // The output uses Unicode arrow → // For uint256 values, should show decoded value "42" @@ -348,6 +380,54 @@ contract StateDiffStorageLayoutTest is DSTest { vm.stopAndReturnStateDiff(); } + function testCompactSlotFormatting() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Test that normal storage slots are formatted compactly + // These writes go to known storage layout positions + simpleStorage.setValue(42); // Slot 0 + simpleStorage.setOwner(address(0xBEEF)); // Slot 1 + + // Get the state diff as string + string memory stateDiff = vm.getStateDiff(); + + emit log_string("State Diff:"); + emit log_string(stateDiff); + + // Check that slot numbers are formatted compactly + assertContains(stateDiff, "@ 0x00 (value", "Slot 0 should be formatted as 0x00"); + assertContains(stateDiff, "@ 0x01 (owner", "Slot 1 should be formatted as 0x01"); + + vm.stopAndReturnStateDiff(); + } + + function testLargeSlotNumberFormatting() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Test with normal slot first (slot 0) + largeSlotStorage.setNormalValue(42); // Slot 0 + + // Test with large slot numbers using assembly + largeSlotStorage.setLargeSlot(999); // Slot 0x123456789abcdef + largeSlotStorage.setMediumSlot(888); // Slot 0x1000 + + // Get the state diff as string + string memory stateDiff = vm.getStateDiff(); + + // The normal storage variable should have layout info + assertContains(stateDiff, "@ 0x00 (normalValue", "Slot 0 should be formatted as 0x00"); + assertContains(stateDiff, unicode"0 → 42", "Should show decoded value 42"); + + // For slots accessed via assembly without storage layout, they'll show raw format + // but the slot numbers should still be compact with even number of hex digits + assertContains(stateDiff, "@ 0x0123456789abcdef:", "Large slot should be compact"); + assertContains(stateDiff, "@ 0x1000:", "Medium slot should be compact with even digits"); + + vm.stopAndReturnStateDiff(); + } + // Helper function to check if a string contains a substring function assertContains(string memory haystack, string memory needle, string memory message) internal pure { bytes memory haystackBytes = bytes(haystack); From 3cfefdc6491f3eca7c167bf9dfb057898a0ac696 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:14:12 +0530 Subject: [PATCH 34/41] encode_prefixed --- crates/cheatcodes/src/evm.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index cae2115cda327..58777544bb91d 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1627,15 +1627,7 @@ fn format_slot(slot: &B256) -> String { } Some(pos) => { // Format from the first non-zero byte onward - let significant_bytes = &slot.0[pos..]; - let hex_str = hex::encode(significant_bytes); - - // Ensure we always have an even number of hex digits (leading zero if odd) - if hex_str.len() % 2 == 1 { - format!("0x0{}", hex_str) - } else { - format!("0x{}", hex_str) - } + hex::encode_prefixed(&slot.0[pos..]) } } } From 850dc6cf755a0bc96ca0bfb778aa2f1deac02dd7 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:15:28 +0530 Subject: [PATCH 35/41] nit --- crates/cheatcodes/src/evm.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 58777544bb91d..ad34fcd95fceb 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1620,15 +1620,11 @@ fn format_slot(slot: &B256) -> String { // Find the first non-zero byte let first_non_zero = slot.0.iter().position(|&b| b != 0); - match first_non_zero { - None => { - // All zeros - display as 0x00 - "0x00".to_string() - } - Some(pos) => { - // Format from the first non-zero byte onward - hex::encode_prefixed(&slot.0[pos..]) - } + if let Some(pos) = first_non_zero { + hex::encode_prefixed(&slot.0[pos..]) + } else { + // All zeros - display as 0x00 + "0x00".to_string() } } From 8ab7b22878efcf4892c712598cbbc10cad6fa34b Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:35:48 +0530 Subject: [PATCH 36/41] fix --- testdata/foundry.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testdata/foundry.toml b/testdata/foundry.toml index ea23af5935fb0..30621914fa353 100644 --- a/testdata/foundry.toml +++ b/testdata/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc = "0.8.18" +# solc = "0.8.18" block_base_fee_per_gas = 0 block_coinbase = "0x0000000000000000000000000000000000000000" block_difficulty = 0 @@ -10,7 +10,7 @@ bytecode_hash = "ipfs" cache = true cache_path = "cache" evm_version = "paris" -extra_output = ["storageLayout"] +extra_output = [] extra_output_files = [] ffi = false force = false From 867944f2e5b112d5e47f1ad5693c0ff6b83ab638 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:35:59 +0530 Subject: [PATCH 37/41] nit --- testdata/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/foundry.toml b/testdata/foundry.toml index 30621914fa353..e9189bb008a32 100644 --- a/testdata/foundry.toml +++ b/testdata/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -# solc = "0.8.18" +solc = "0.8.18" block_base_fee_per_gas = 0 block_coinbase = "0x0000000000000000000000000000000000000000" block_difficulty = 0 From e580998f7862e2ae6cec9bc5e7d7bee8c12ec560 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:21:49 +0530 Subject: [PATCH 38/41] Revert "encode_prefixed" This reverts commit 3cfefdc6491f3eca7c167bf9dfb057898a0ac696. --- crates/cheatcodes/src/evm.rs | 14 ---- .../cheats/StateDiffStorageLayout.t.sol | 77 ------------------- 2 files changed, 91 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index ad34fcd95fceb..1eae391b23cff 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -216,7 +216,6 @@ impl Display for AccountStateDiffs { if !&self.state_diff.is_empty() { writeln!(f, "- state diff:")?; for (slot, slot_changes) in &self.state_diff { - let slot = format_slot(slot); match (&slot_changes.slot_info, &slot_changes.decoded) { (Some(slot_info), Some(decoded)) => { // Have both slot info and decoded values - only show decoded values @@ -1615,19 +1614,6 @@ fn decode_storage_value(value: B256, dyn_type: &DynSolType) -> Option String { - // Find the first non-zero byte - let first_non_zero = slot.0.iter().position(|&b| b != 0); - - if let Some(pos) = first_non_zero { - hex::encode_prefixed(&slot.0[pos..]) - } else { - // All zeros - display as 0x00 - "0x00".to_string() - } -} - /// Helper function to format DynSolValue as raw string without type information fn format_dyn_sol_value_raw(value: &DynSolValue) -> String { match value { diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 54e381c8df4ae..d8f96689aee86 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -97,41 +97,17 @@ contract TwoDArrayStorage { } } -contract LargeSlotStorage { - // Regular storage variables - uint256 public normalValue; // slot 0 - - function setLargeSlot(uint256 value) external { - assembly { - // Using a large slot number directly - sstore(0x123456789abcdef, value) - } - } - - function setMediumSlot(uint256 value) external { - assembly { - // Using a medium slot number (4096) - sstore(0x1000, value) - } - } - - function setNormalValue(uint256 value) external { - normalValue = value; - } -} contract StateDiffStorageLayoutTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); SimpleStorage simpleStorage; VariousArrays variousArrays; TwoDArrayStorage twoDArrayStorage; - LargeSlotStorage largeSlotStorage; function setUp() public { simpleStorage = new SimpleStorage(); variousArrays = new VariousArrays(); twoDArrayStorage = new TwoDArrayStorage(); - largeSlotStorage = new LargeSlotStorage(); } function testSimpleStorageLayout() public { @@ -357,12 +333,6 @@ contract StateDiffStorageLayoutTest is DSTest { // Get the state diff as string (not JSON) string memory stateDiff = vm.getStateDiff(); - // Check that slots are formatted compactly (e.g., @ 0x00, @ 0x01, @ 0x02) - assertContains(stateDiff, "@ 0x00 (value", "Slot 0 should be formatted as 0x00"); - assertContains(stateDiff, "@ 0x01 (owner", "Slot 1 should be formatted as 0x01"); - assertContains(stateDiff, "@ 0x02 (values[0]", "Slot 2 should be formatted as 0x02"); - assertContains(stateDiff, "@ 0x03 (values[1]", "Slot 3 should be formatted as 0x03"); - assertContains(stateDiff, "@ 0x04 (values[2]", "Slot 4 should be formatted as 0x04"); // Test that decoded values are shown in the string format // The output uses Unicode arrow → @@ -380,53 +350,6 @@ contract StateDiffStorageLayoutTest is DSTest { vm.stopAndReturnStateDiff(); } - function testCompactSlotFormatting() public { - // Start recording state diffs - vm.startStateDiffRecording(); - - // Test that normal storage slots are formatted compactly - // These writes go to known storage layout positions - simpleStorage.setValue(42); // Slot 0 - simpleStorage.setOwner(address(0xBEEF)); // Slot 1 - - // Get the state diff as string - string memory stateDiff = vm.getStateDiff(); - - emit log_string("State Diff:"); - emit log_string(stateDiff); - - // Check that slot numbers are formatted compactly - assertContains(stateDiff, "@ 0x00 (value", "Slot 0 should be formatted as 0x00"); - assertContains(stateDiff, "@ 0x01 (owner", "Slot 1 should be formatted as 0x01"); - - vm.stopAndReturnStateDiff(); - } - - function testLargeSlotNumberFormatting() public { - // Start recording state diffs - vm.startStateDiffRecording(); - - // Test with normal slot first (slot 0) - largeSlotStorage.setNormalValue(42); // Slot 0 - - // Test with large slot numbers using assembly - largeSlotStorage.setLargeSlot(999); // Slot 0x123456789abcdef - largeSlotStorage.setMediumSlot(888); // Slot 0x1000 - - // Get the state diff as string - string memory stateDiff = vm.getStateDiff(); - - // The normal storage variable should have layout info - assertContains(stateDiff, "@ 0x00 (normalValue", "Slot 0 should be formatted as 0x00"); - assertContains(stateDiff, unicode"0 → 42", "Should show decoded value 42"); - - // For slots accessed via assembly without storage layout, they'll show raw format - // but the slot numbers should still be compact with even number of hex digits - assertContains(stateDiff, "@ 0x0123456789abcdef:", "Large slot should be compact"); - assertContains(stateDiff, "@ 0x1000:", "Medium slot should be compact with even digits"); - - vm.stopAndReturnStateDiff(); - } // Helper function to check if a string contains a substring function assertContains(string memory haystack, string memory needle, string memory message) internal pure { From f3666670867df27cc5ee362cefc247f9fa569523 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:22:37 +0530 Subject: [PATCH 39/41] Revert "feat: format slots to only significant bits in vm.getStateDiff output" This reverts commit aa3602202027f4041a110fcb5873d4b89b1168e5. --- testdata/default/cheats/StateDiffStorageLayout.t.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index d8f96689aee86..961a2cd4b4363 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -97,7 +97,6 @@ contract TwoDArrayStorage { } } - contract StateDiffStorageLayoutTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); SimpleStorage simpleStorage; @@ -333,7 +332,6 @@ contract StateDiffStorageLayoutTest is DSTest { // Get the state diff as string (not JSON) string memory stateDiff = vm.getStateDiff(); - // Test that decoded values are shown in the string format // The output uses Unicode arrow → // For uint256 values, should show decoded value "42" @@ -350,7 +348,6 @@ contract StateDiffStorageLayoutTest is DSTest { vm.stopAndReturnStateDiff(); } - // Helper function to check if a string contains a substring function assertContains(string memory haystack, string memory needle, string memory message) internal pure { bytes memory haystackBytes = bytes(haystack); From 9b733a79ea03875db46a830da09b49fa1cb9ccc6 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:31:56 +0530 Subject: [PATCH 40/41] docs --- crates/cheatcodes/src/evm.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 1eae391b23cff..acb51a6e51958 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -106,20 +106,28 @@ struct SlotStateDiff { previous_value: B256, /// Current storage value. new_value: B256, - /// Decoded Slot Values + /// Decoded values according to the Solidity type (e.g., uint256, address). + /// Only present when storage layout is available and decoding succeeds. #[serde(skip_serializing_if = "Option::is_none")] decoded: Option, - /// Slot Info + + /// Storage layout metadata (variable name, type, offset). + /// Only present when contract has storage layout output. #[serde(skip_serializing_if = "Option::is_none", flatten)] slot_info: Option, } +/// Storage slot metadata from the contract's storage layout. #[derive(Serialize, Debug)] struct SlotInfo { + /// Variable name (e.g., "owner", "values[0]", "config.maxSize"). label: String, + /// Solidity type, serialized as string (e.g., "uint256", "address"). #[serde(rename = "type", serialize_with = "serialize_dyn_sol_type")] dyn_sol_type: DynSolType, + /// Byte offset within the 32-byte slot (0 for full slot, 0-31 for packed). offset: i64, + /// Storage slot number as decimal string. slot: String, } @@ -130,11 +138,12 @@ where serializer.serialize_str(&dyn_type.to_string()) } +/// Decoded storage values showing before and after states. #[derive(Debug)] struct DecodedSlotValues { - /// Initial decoded storage value + /// Decoded value before the state change. previous_value: DynSolValue, - /// Current decoded storage value + /// Decoded value after the state change. new_value: DynSolValue, } From b784a514c418fa760ca20f88a85291d902946f50 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:35:29 +0530 Subject: [PATCH 41/41] doc fix --- crates/cheatcodes/src/evm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index acb51a6e51958..31922b8194c41 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -120,7 +120,7 @@ struct SlotStateDiff { /// Storage slot metadata from the contract's storage layout. #[derive(Serialize, Debug)] struct SlotInfo { - /// Variable name (e.g., "owner", "values[0]", "config.maxSize"). + /// Variable name (e.g., "owner", "values\[0\]", "config.maxSize"). label: String, /// Solidity type, serialized as string (e.g., "uint256", "address"). #[serde(rename = "type", serialize_with = "serialize_dyn_sol_type")]