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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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 17:42:12 +0530 Subject: [PATCH 30/55] feat(cheatcodes): decode structs in state diff output --- crates/cheatcodes/Cargo.toml | 2 +- crates/cheatcodes/src/evm.rs | 487 ++++++++++++++++-- .../default/cheats/StateDiffStructTest.t.sol | 417 +++++++++++++++ 3 files changed, 863 insertions(+), 43 deletions(-) create mode 100644 testdata/default/cheats/StateDiffStructTest.t.sol diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index f62bde4323e71..4c3528c9ade83 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -24,7 +24,7 @@ foundry-evm-traces.workspace = true foundry-wallets.workspace = true forge-script-sequence.workspace = true -alloy-dyn-abi.workspace = true +alloy-dyn-abi = { workspace = true, features = ["eip712"] } alloy-evm.workspace = true alloy-json-abi.workspace = true alloy-primitives.workspace = true diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 4b52419554943..d6b47df32f9e9 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -8,11 +8,11 @@ use crate::{ use alloy_consensus::TxEnvelope; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_genesis::{Genesis, GenesisAccount}; -use alloy_primitives::{Address, B256, U256, hex, map::HashMap}; +use alloy_primitives::{Address, B256, U256, hex, keccak256, 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::StorageLayout; +use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType}; use foundry_evm_core::{ ContextExt, backend::{DatabaseExt, RevertStateSnapshotAction}, @@ -117,17 +117,44 @@ struct SlotStateDiff { #[derive(Serialize, Debug)] struct SlotInfo { label: String, - #[serde(rename = "type", serialize_with = "serialize_dyn_sol_type")] - dyn_sol_type: DynSolType, + #[serde(rename = "type", serialize_with = "serialize_slot_type")] + slot_type: SlotType, offset: i64, slot: String, + /// For struct members, contains nested SlotInfo for each member + #[serde(skip_serializing_if = "Option::is_none")] + members: Option>, + /// Decoded values (if available) - used for struct members + #[serde(skip_serializing_if = "Option::is_none")] + decoded: Option, } -fn serialize_dyn_sol_type(dyn_type: &DynSolType, serializer: S) -> Result +/// Wrapper type that holds both the original type label and the DynSolType +#[derive(Debug)] +struct SlotType { + /// Original type label from storage layout (e.g., "struct DiffTest.TestStruct") + label: String, + /// Parsed dynamic type for decoding + dyn_sol_type: DynSolType, +} + +fn serialize_slot_type(slot_type: &SlotType, serializer: S) -> Result where S: serde::Serializer, { - serializer.serialize_str(&dyn_type.to_string()) + // For CustomStruct, format as "struct Name", otherwise use the label + let type_str = match &slot_type.dyn_sol_type { + DynSolType::CustomStruct { name, .. } => { + // If the label already has "struct " prefix, use it as-is + if slot_type.label.starts_with("struct ") { + slot_type.label.clone() + } else { + format!("struct {}", name) + } + } + _ => slot_type.label.clone(), + }; + serializer.serialize_str(&type_str) } #[derive(Debug)] @@ -223,7 +250,7 @@ impl Display for AccountStateDiffs { f, "@ {slot} ({}, {}): {} → {} [decoded: {} → {}]", slot_info.label, - slot_info.dyn_sol_type, + slot_info.slot_type.dyn_sol_type, slot_changes.previous_value, slot_changes.new_value, format_dyn_sol_value_raw(&decoded.previous_value), @@ -236,7 +263,7 @@ impl Display for AccountStateDiffs { f, "@ {slot} ({}, {}): {} → {}", slot_info.label, - slot_info.dyn_sol_type, + slot_info.slot_type.dyn_sol_type, slot_changes.previous_value, slot_changes.new_value )?; @@ -1327,10 +1354,20 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap BTreeMap bits / 8, + DynSolType::Address => 20, + DynSolType::Bool => 1, + DynSolType::FixedBytes(size) => *size, + _ => 32, // Default to full word + }; + + // Extract and decode member values + let mut prev_bytes = [0u8; 32]; + let mut new_bytes = [0u8; 32]; + + if offset + size <= 32 { + // In Solidity storage, values are right-aligned + // For offset 0, we want the rightmost bytes + // For offset 16 (for a uint128), we want bytes + // 0-16 + let byte_offset = 32 - offset - size; + prev_bytes[32 - size..].copy_from_slice( + &storage_access.previousValue.0 + [byte_offset..byte_offset + size], + ); + new_bytes[32 - size..].copy_from_slice( + &storage_access.newValue.0 + [byte_offset..byte_offset + size], + ); + } + + // Decode the member values + if let (Ok(prev_val), Ok(new_val)) = ( + member + .slot_type + .dyn_sol_type + .abi_decode(&prev_bytes), + member + .slot_type + .dyn_sol_type + .abi_decode(&new_bytes), + ) { + member.decoded = Some(DecodedSlotValues { + previous_value: prev_val, + new_value: new_val, + }); + } + } + // For structs with members, we don't need a top-level + // decoded value + (None, Some(info)) + } else { + // Not a struct, decode as a single value + let storage_layout = + storage_layouts.get(&storage_access.account); + let storage_type = storage_layout.and_then(|layout| { + layout + .storage + .iter() + .find(|s| s.slot == info.slot) + .and_then(|s| layout.types.get(&s.storage_type)) + }); + + let decoded = if let (Some(prev), Some(new)) = ( + decode_storage_value( + storage_access.previousValue, + &info.slot_type.dyn_sol_type, + storage_type, + storage_layout.as_ref().map(|arc| arc.as_ref()), + ), + decode_storage_value( + storage_access.newValue, + &info.slot_type.dyn_sol_type, + storage_type, + storage_layout.as_ref().map(|arc| arc.as_ref()), + ), + ) { + Some(DecodedSlotValues { + previous_value: prev, + new_value: new, + }) + } else { + None + }; + (decoded, Some(info)) + } + } else { + (None, None) + }; slot_state_diff.insert(SlotStateDiff { previous_value: storage_access.previousValue, new_value: storage_access.newValue, decoded, - slot_info, + slot_info: slot_info_with_decoded, }); } Entry::Occupied(mut slot_state_diff) => { @@ -1431,12 +1561,27 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap( // Try to find the artifact by deployed code let code_bytes = code.original_bytes(); + trace!( + "Looking for artifact for address {:?} with code hash: {:?}", + address, + keccak256(&code_bytes) + ); if let Some(result) = artifacts.find_by_deployed_code_exact(&code_bytes) { + trace!("Found exact match for {:?}: {:?}", address, result.0); return Some(result); } // Fallback to fuzzy matching if exact match fails - artifacts.find_by_deployed_code(&code_bytes) + let fuzzy_result = artifacts.find_by_deployed_code(&code_bytes); + if let Some(ref result) = fuzzy_result { + trace!("Found fuzzy match for {:?}: {:?}", address, result.0); + } else { + trace!("No match found for {:?}", address); + } + fuzzy_result } /// Gets storage layout info for a specific slot. fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { let slot = U256::from_be_bytes(slot.0); let slot_str = slot.to_string(); + trace!("Looking for slot {} in storage layout", slot_str); for storage in &storage_layout.storage { - 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()?; + trace!( + "Checking storage item: label={}, slot={}, type={}", + storage.label, storage.slot, storage.storage_type + ); + let base_slot = match U256::from_str(&storage.slot) { + Ok(s) => s, + Err(e) => { + trace!("Failed to parse slot '{}': {:?}", storage.slot, e); + continue; + } + }; + let storage_type = match storage_layout.types.get(&storage.storage_type) { + Some(t) => t, + None => { + trace!("Storage type '{}' not found in types", storage.storage_type); + continue; + } + }; + + // Parse the type - structs need special handling + let mut parsed_type = if storage_type.label.starts_with("struct ") { + // For structs, we need to build the type from members + if let Some(members_value) = storage_type.other.get("members") + && let Ok(members) = serde_json::from_value::>(members_value.clone()) + { + let mut member_types = Vec::new(); + for member in &members { + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && let Ok(member_type) = DynSolType::parse(&member_type_info.label) + { + member_types.push(member_type); + } else { + // If we can't parse all members, skip this item + continue; + } + } + // Create a tuple type for now, will convert to CustomStruct below + DynSolType::Tuple(member_types) + } else { + trace!("Failed to get members for struct type '{}'", storage_type.label); + continue; + } + } else { + // Non-struct types can be parsed directly + match DynSolType::parse(&storage_type.label) { + Ok(t) => t, + Err(e) => { + trace!("Failed to parse type label '{}': {:?}", storage_type.label, e); + continue; + } + } + }; + + // If it's a struct, enhance it with CustomStruct variant + if storage_type.label.starts_with("struct ") { + if let Some(struct_name) = storage_type.label.strip_prefix("struct ") + && let Some(members_value) = storage_type.other.get("members") + && let Ok(members) = serde_json::from_value::>(members_value.clone()) + { + let mut prop_names = Vec::new(); + let mut member_types = Vec::new(); + + for member in &members { + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && let Ok(member_type) = DynSolType::parse(&member_type_info.label) + { + prop_names.push(member.label.clone()); + member_types.push(member_type); + } + } + + // Only create CustomStruct if we successfully parsed all members + if member_types.len() == members.len() { + parsed_type = DynSolType::CustomStruct { + name: struct_name.to_string(), + prop_names, + tuple: member_types, + }; + } + } + } + + trace!("Parsed type '{}' for slot {} as: {:?}", storage_type.label, slot_str, parsed_type); + + // Build struct members info if this is a struct with members + // Only include members for single-slot structs (where all members are in the same slot) + let struct_members = if matches!(parsed_type, DynSolType::CustomStruct { .. }) + && let Some(members_value) = storage_type.other.get("members") + && let Ok(members) = serde_json::from_value::>(members_value.clone()) + { + // Check if all members are in the same slot (single-slot struct) + let all_same_slot = members.iter().all(|m| m.slot == "0"); + + if all_same_slot { + // Single-slot struct - include member info for decoding + let mut member_infos = Vec::new(); + + for member in &members { + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && let Ok(member_type) = DynSolType::parse(&member_type_info.label) + { + member_infos.push(SlotInfo { + label: member.label.clone(), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); + } + } + + if !member_infos.is_empty() { Some(member_infos) } else { None } + } else { + // Multi-slot struct - don't include members here + // Each member will be handled as a separate slot access + None + } + } else { + None + }; // Check for exact slot match if storage.slot == slot_str { - let label = match &dyn_type { + // For multi-slot structs at base slot, decode the first member + if matches!(parsed_type, DynSolType::CustomStruct { .. }) + && struct_members.is_none() // This is a multi-slot struct + && let Some(members_value) = storage_type.other.get("members") + && let Ok(members) = serde_json::from_value::>(members_value.clone()) + && !members.is_empty() + { + // Find the first member (at slot offset 0) + if let Some(first_member) = members.iter().find(|m| m.slot == "0") { + if let Some(member_type_info) = storage_layout.types.get(&first_member.storage_type) + && let Ok(member_type) = DynSolType::parse(&member_type_info.label) + { + // Return info for the first member instead of the struct + return Some(SlotInfo { + label: format!("{}.{}", storage.label, first_member.label), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: first_member.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); + } + } + } + + let label = match &parsed_type { DynSolType::FixedArray(_, _) => { // For arrays, label the base slot with indices - format!("{}{}", storage.label, get_array_base_indices(&dyn_type)) + format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) } _ => storage.label.clone(), }; return Some(SlotInfo { label, - dyn_sol_type: dyn_type, + slot_type: SlotType { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, offset: storage.offset, slot: storage.slot.clone(), + members: struct_members, + decoded: None, // Will be populated when decoding }); } // Check if slot is part of a static array - if let DynSolType::FixedArray(_, _) = &dyn_type + if let DynSolType::FixedArray(_, _) = &parsed_type && let Ok(total_bytes) = storage_type.number_of_bytes.parse::() { let total_slots = total_bytes.div_ceil(32); @@ -1539,16 +1851,57 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option 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); + let label = format_array_element_label(&storage.label, &parsed_type, index); return Some(SlotInfo { label, - dyn_sol_type: dyn_type, + slot_type: SlotType { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, offset: 0, slot: slot.to_string(), + members: None, // Arrays don't have members like structs + decoded: None, // Will be populated when decoding }); } } + + // Check if this slot is part of a multi-slot struct + // For structs that span multiple slots, check if the requested slot + // matches a member's absolute slot position + if storage_type.label.starts_with("struct ") + && let Some(members_value) = storage_type.other.get("members") + && let Ok(members) = serde_json::from_value::>(members_value.clone()) + { + for member in &members { + // Calculate absolute slot for this member + let member_slot = if let Ok(member_slot_offset) = U256::from_str(&member.slot) { + base_slot + member_slot_offset + } else { + continue; + }; + + // Check if this is the slot we're looking for + if member_slot == slot { + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && let Ok(member_type) = DynSolType::parse(&member_type_info.label) + { + return Some(SlotInfo { + label: format!("{}.{}", storage.label, member.label), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str.clone(), + members: None, + decoded: None, + }); + } + } + } + } } None @@ -1588,7 +1941,12 @@ 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 { +fn decode_storage_value( + value: B256, + dyn_type: &DynSolType, + storage_type: Option<&StorageType>, + storage_layout: Option<&StorageLayout>, +) -> 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; @@ -1598,7 +1956,52 @@ fn decode_storage_value(value: B256, dyn_type: &DynSolType) -> Option>(members_value.clone()) + && members.len() == member_types.len() + { + let mut decoded_members = Vec::new(); + + for (i, member) in members.iter().enumerate() { + // Get the member type + let member_type = &member_types[i]; + + // Calculate byte range for this member + let offset = member.offset as usize; + let member_storage_type = + storage_layout.and_then(|sl| sl.types.get(&member.storage_type)); + let size = member_storage_type + .and_then(|t| t.number_of_bytes.parse::().ok()) + .unwrap_or(32); + + // Extract bytes for this member from the full value + let mut member_bytes = [0u8; 32]; + if offset + size <= 32 { + // In Solidity storage, values are right-aligned + // For offset 0, we want the rightmost bytes + // For offset 16 (for a uint128), we want bytes 0-16 + let byte_offset = 32 - offset - size; + member_bytes[32 - size..] + .copy_from_slice(&value.0[byte_offset..byte_offset + size]); + } + + // Decode the member value + if let Ok(decoded) = member_type.abi_decode(&member_bytes) { + decoded_members.push(decoded); + } else { + return None; + } + } + + return Some(DynSolValue::Tuple(decoded_members)); + } + } + + // Use abi_decode to decode the value for non-struct types actual_type.abi_decode(&value.0).ok() } diff --git a/testdata/default/cheats/StateDiffStructTest.t.sol b/testdata/default/cheats/StateDiffStructTest.t.sol new file mode 100644 index 0000000000000..8a61fda7a1017 --- /dev/null +++ b/testdata/default/cheats/StateDiffStructTest.t.sol @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract DiffTest { + struct TestStruct { + uint128 a; + uint128 b; + } + + // Multi-slot struct (spans 3 slots) + struct MultiSlotStruct { + uint256 value1; // slot 0 + address addr; // slot 1 (takes 20 bytes, but uses full slot) + uint256 value2; // slot 2 + } + + // Nested struct with MultiSlotStruct as inner + struct NestedStruct { + MultiSlotStruct inner; // slots 0-2 (spans 3 slots) + uint256 value; // slot 3 + address owner; // slot 4 + } + + TestStruct internal testStruct; + MultiSlotStruct internal multiSlotStruct; + NestedStruct internal nestedStruct; + + function setStruct(uint128 a, uint128 b) public { + testStruct.a = a; + testStruct.b = b; + } + + function setMultiSlotStruct(uint256 v1, address a, uint256 v2) public { + multiSlotStruct.value1 = v1; + multiSlotStruct.addr = a; + multiSlotStruct.value2 = v2; + } + + function setNestedStruct( + uint256 v1, + address a, + uint256 v2, + uint256 v, + address o + ) public { + nestedStruct.inner.value1 = v1; + nestedStruct.inner.addr = a; + nestedStruct.inner.value2 = v2; + nestedStruct.value = v; + nestedStruct.owner = o; + } +} + +contract StateDiffStructTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + DiffTest internal test; + + function setUp() public { + test = new DiffTest(); + } + + function testStruct() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Set struct values: a=1, b=2 + test.setStruct(1, 2); + + // Get the state diff as JSON + string memory stateDiffJson = vm.getStateDiffJson(); + + // Check that the struct is properly labeled + assertContains( + stateDiffJson, + '"label":"testStruct"', + "Should contain 'testStruct' label" + ); + + // Check that the type is correctly identified as a struct + assertContains( + stateDiffJson, + '"type":"struct DiffTest.TestStruct"', + "Should contain struct type" + ); + + // Check for members field - structs have members with individual decoded values + assertContains( + stateDiffJson, + '"members":', + "Should contain members field for struct" + ); + + // Check that member 'a' is properly decoded + assertContains( + stateDiffJson, + '"label":"a"', + "Should contain member 'a' label" + ); + assertContains( + stateDiffJson, + '"type":"uint128"', + "Should contain uint128 type for members" + ); + + // Check that member 'b' is properly decoded + assertContains( + stateDiffJson, + '"label":"b"', + "Should contain member 'b' label" + ); + + // The members should have decoded values + // Check specific decoded values for each member in the members array + // Member 'a' at offset 0 should have previous value 0 and new value 1 + assertContains( + stateDiffJson, + '{"label":"a","type":"uint128","offset":0,"slot":"0","decoded":{"previousValue":"0","newValue":"1"}}', + "Member 'a' should be decoded with previous=0, new=1" + ); + + // Member 'b' at offset 16 should have previous value 0 and new value 2 + assertContains( + stateDiffJson, + '{"label":"b","type":"uint128","offset":16,"slot":"0","decoded":{"previousValue":"0","newValue":"2"}}', + "Member 'b' should be decoded with previous=0, new=2" + ); + + // Verify the raw storage values are correct + // The storage layout packs uint128 a at offset 0 and uint128 b at offset 16 + // So the value 0x0000000000000000000200000000000000000000000000000001 represents: + // - First 16 bytes (a): 0x0000000000000000000000000000000001 = 1 + // - Last 16 bytes (b): 0x0000000000000000000000000000000002 = 2 + assertContains( + stateDiffJson, + '"0x0000000000000000000000000000000200000000000000000000000000000001"', + "Should contain the correct packed storage value" + ); + + // Stop recording and verify we get the expected account accesses + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + assertTrue(accesses.length > 0, "Should have account accesses"); + + // Find the storage access for our struct + bool foundStructAccess = false; + for (uint256 i = 0; i < accesses.length; i++) { + if (accesses[i].account == address(test)) { + for ( + uint256 j = 0; + j < accesses[i].storageAccesses.length; + j++ + ) { + Vm.StorageAccess memory access = accesses[i] + .storageAccesses[j]; + if (access.slot == bytes32(uint256(0)) && access.isWrite) { + foundStructAccess = true; + // Verify the storage values + assertEq( + access.previousValue, + bytes32(uint256(0)), + "Previous value should be 0" + ); + assertEq( + access.newValue, + bytes32( + uint256( + 0x0000000000000000000200000000000000000000000000000001 + ) + ), + "New value should pack a=1 and b=2" + ); + } + } + } + } + + assertTrue( + foundStructAccess, + "Should have found struct storage access" + ); + } + + function testMultiSlotStruct() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Set multi-slot struct values + test.setMultiSlotStruct( + 123456789, // value1 + address(0xdEaDbEeF), // addr + 987654321 // value2 + ); + + // Get the state diff as JSON + string memory stateDiffJson = vm.getStateDiffJson(); + + // Debug: log the JSON for inspection + emit log_string("State diff JSON:"); + emit log_string(stateDiffJson); + + // Check that the struct's first member is properly labeled + assertContains( + stateDiffJson, + '"label":"multiSlotStruct.value1"', + "Should contain 'multiSlotStruct.value1' label" + ); + + // For multi-slot structs, the base slot now shows the first member's type + // The struct type itself is not shown since we decode the first member directly + + // Multi-slot structs don't have members field in the base slot + // Instead, each member appears as a separate slot entry with dotted labels + + // Check that each member slot is properly labeled + // Note: slot 1 now shows multiSlotStruct.value1 since it's the first member + assertContains( + stateDiffJson, + '"label":"multiSlotStruct.value1"', + "Should contain multiSlotStruct.value1 label for first slot" + ); + assertContains( + stateDiffJson, + '"label":"multiSlotStruct.addr"', + "Should contain member 'addr' label" + ); + assertContains( + stateDiffJson, + '"label":"multiSlotStruct.value2"', + "Should contain member 'value2' label" + ); + + // Check member types + assertContains( + stateDiffJson, + '"type":"uint256"', + "Should contain uint256 type" + ); + assertContains( + stateDiffJson, + '"type":"address"', + "Should contain address type" + ); + + // Check that value1 is properly decoded from slot 1 + assertContains( + stateDiffJson, + '"decoded":{"previousValue":"0","newValue":"123456789"}', + "value1 should be decoded from slot 1" + ); + + // Also verify the raw hex value + assertContains( + stateDiffJson, + "0x00000000000000000000000000000000000000000000000000000000075bcd15", + "Slot 1 should contain value1 in hex" + ); + + // Slot 2 should have the address decoded + assertContains( + stateDiffJson, + '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x00000000000000000000000000000000DeaDBeef"}', + "Address should be decoded from slot 2" + ); + + // Slot 3 should have value2 decoded + assertContains( + stateDiffJson, + '"decoded":{"previousValue":"0","newValue":"987654321"}', + "Value2 should be decoded from slot 3" + ); + + // Stop recording + vm.stopAndReturnStateDiff(); + } + + function testNestedStruct() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Set nested struct values - now with MultiSlotStruct as inner + test.setNestedStruct( + 111111111, // inner.value1 + address(0xCAFE), // inner.addr + 222222222, // inner.value2 + 333333333, // value + address(0xBEEF) // owner + ); + + // Get the state diff as JSON + string memory stateDiffJson = vm.getStateDiffJson(); + + // Check that the struct is properly labeled + assertContains( + stateDiffJson, + '"label":"nestedStruct"', + "Should contain 'nestedStruct' label" + ); + + // Check that the type is correctly identified as a struct + assertContains( + stateDiffJson, + '"type":"struct DiffTest.NestedStruct"', + "Should contain struct type" + ); + + // Nested struct with multi-slot inner struct doesn't have members field either + // Each member appears as a separate slot + + // Check that nested struct labels are properly set + assertContains( + stateDiffJson, + '"label":"nestedStruct"', + "Should contain nestedStruct label" + ); + + // Check other members have proper labels + assertContains( + stateDiffJson, + '"label":"nestedStruct.value"', + "Should contain member 'value' label" + ); + assertContains( + stateDiffJson, + '"label":"nestedStruct.owner"', + "Should contain member 'owner' label" + ); + + // The inner struct members are in slots 4, 5, 6 but we only see their storage diffs + // They don't appear with member labels in this test since they're part of the nested struct + + // Check that slot 4 has the first value + assertContains( + stateDiffJson, + "0x00000000000000000000000000000000000000000000000000000000069f6bc7", + "Slot 4 should contain inner.value1 in hex" + ); + // Note: addresses in slots 5 and 6 may not have labels due to nested struct complexity + // But the important values are decoded correctly + + // Check decoded values for outer struct members + // Slot 7 should have nestedStruct.value decoded with previous=0 and new=333333333 + assertContains( + stateDiffJson, + '"label":"nestedStruct.value"', + "Should have nestedStruct.value label" + ); + assertContains( + stateDiffJson, + '"slot":"7"', + "nestedStruct.value should be in slot 7" + ); + assertContains( + stateDiffJson, + '"previousValue":"0","newValue":"333333333"', + "Should decode nestedStruct.value correctly" + ); + + // Slot 8 should have nestedStruct.owner decoded + assertContains( + stateDiffJson, + '"label":"nestedStruct.owner"', + "Should have nestedStruct.owner label" + ); + assertContains( + stateDiffJson, + '"slot":"8"', + "nestedStruct.owner should be in slot 8" + ); + assertContains( + stateDiffJson, + '"newValue":"0x000000000000000000000000000000000000bEEF"', + "Should decode owner address correctly" + ); + + // Stop recording + 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 0db68f77fd69fd496f16ca11b1e705172bd0ffe6 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:03:08 +0530 Subject: [PATCH 31/55] 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 32/55] 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 33/55] 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 34/55] 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 35/55] 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 36/55] 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 6e3120b79663aed5eb7be388099fed1bbadd7779 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Tue, 19 Aug 2025 03:50:46 -0700 Subject: [PATCH 37/55] chore: add @onbjerg to `CODEOWNERS` (#11343) * add @onbjerg * add @0xrusowsky --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 63b25bbbcb260..aefd74a85494a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks +* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg @0xrusowsky From 0ea49a267b05751d8161098413f2abdb84d8e223 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:53:11 +0530 Subject: [PATCH 38/55] resolve conflicts --- crates/cheatcodes/src/evm.rs | 39 +- .../cheats/StateDiffStorageLayout.t.sol | 469 +++++++++++++++--- 2 files changed, 400 insertions(+), 108 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index af29b60be6bb6..16e388078e6a1 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -106,10 +106,13 @@ 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, } @@ -1367,20 +1370,10 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap( // Try to find the artifact by deployed code let code_bytes = code.original_bytes(); - trace!( - "Looking for artifact for address {:?} with code hash: {:?}", - address, - keccak256(&code_bytes) - ); if let Some(result) = artifacts.find_by_deployed_code_exact(&code_bytes) { - trace!("Found exact match for {:?}: {:?}", address, result.0); return Some(result); } // Fallback to fuzzy matching if exact match fails - let fuzzy_result = artifacts.find_by_deployed_code(&code_bytes); - if let Some(ref result) = fuzzy_result { - trace!("Found fuzzy match for {:?}: {:?}", address, result.0); - } else { - trace!("No match found for {:?}", address); - } - fuzzy_result + artifacts.find_by_deployed_code(&code_bytes) } /// Gets storage layout info for a specific slot. diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 54e381c8df4ae..d8693c8dfa813 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -86,7 +86,11 @@ contract TwoDArrayStorage { matrix[i][j] = value; } - function setAddresses2D(address[2] memory row0, address[2] memory row1, address[2] memory row2) public { + function setAddresses2D( + address[2] memory row0, + address[2] memory row1, + address[2] memory row2 + ) public { addresses2D[0] = row0; addresses2D[1] = row1; addresses2D[2] = row2; @@ -151,31 +155,86 @@ 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(); - assertTrue(accesses.length >= 3, "Should have at least 3 account accesses for the calls"); + assertTrue( + accesses.length >= 3, + "Should have at least 3 account accesses for the calls" + ); // Verify storage accesses for SimpleStorage bool foundValueSlot = false; @@ -186,7 +245,11 @@ contract StateDiffStorageLayoutTest is DSTest { for (uint256 i = 0; i < accesses.length; i++) { if (accesses[i].account == address(simpleStorage)) { - for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + 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; @@ -218,32 +281,88 @@ 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 @@ -262,12 +381,36 @@ 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(); } @@ -287,24 +430,80 @@ 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(); } @@ -327,20 +526,60 @@ 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(); } @@ -358,24 +597,64 @@ contract StateDiffStorageLayoutTest is DSTest { 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"); + 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" - assertContains(stateDiff, unicode"0 → 42", "Should show decoded uint256 value"); + assertContains( + stateDiff, + unicode"0 → 42", + "Should show decoded uint256 value" + ); // For addresses, should show decoded address format - assertContains(stateDiff, "0x000000000000000000000000000000000000bEEF", "Should show decoded address"); + 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"); + 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(); } @@ -396,8 +675,16 @@ contract StateDiffStorageLayoutTest is DSTest { 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"); + assertContains( + stateDiff, + "@ 0x00 (value", + "Slot 0 should be formatted as 0x00" + ); + assertContains( + stateDiff, + "@ 0x01 (owner", + "Slot 1 should be formatted as 0x01" + ); vm.stopAndReturnStateDiff(); } @@ -417,19 +704,39 @@ contract StateDiffStorageLayoutTest is DSTest { 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"); + 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"); + 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 { + function assertContains( + string memory haystack, + string memory needle, + string memory message + ) internal pure { bytes memory haystackBytes = bytes(haystack); bytes memory needleBytes = bytes(needle); @@ -438,7 +745,11 @@ contract StateDiffStorageLayoutTest is DSTest { } bool found = false; - for (uint256 i = 0; i <= haystackBytes.length - needleBytes.length; i++) { + 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]) { From a11c347c97bb3f9f5dd1509feb1bea99646bc5ad Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 19 Aug 2025 17:46:16 +0200 Subject: [PATCH 39/55] fix: disable tx gas limit cap (#11347) --- crates/evm/core/src/fork/init.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/evm/core/src/fork/init.rs b/crates/evm/core/src/fork/init.rs index 4742d46fcaad2..ddb82bf7798da 100644 --- a/crates/evm/core/src/fork/init.rs +++ b/crates/evm/core/src/fork/init.rs @@ -92,5 +92,7 @@ pub fn configure_env(chain_id: u64, memory_limit: u64, disable_block_gas_limit: cfg.disable_eip3607 = true; cfg.disable_block_gas_limit = disable_block_gas_limit; cfg.disable_nonce_check = true; + // For Osaka EIP-7825 + cfg.tx_gas_limit_cap = Some(u64::MAX); cfg } From f9964de97de7ed0da1c2c051e8bc401c44aad808 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:34:09 +0200 Subject: [PATCH 40/55] chore(deps): bump all dependencies (#11349) --- Cargo.lock | 178 +++++++++++++++++++++++++++-------------------------- 1 file changed, 91 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d9694f83a7be..5f88c98e14be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4195a29a4b87137b2bb02105e746102873bc03561805cf45c0e510c961f160e6" +checksum = "a379c0d821498c996ceb9e7519fc2dab8286c35a203c1fb95f80ecd66e07cf2f" dependencies = [ "alloy-primitives", "num_enum", @@ -70,9 +70,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda689f7287f15bd3582daba6be8d1545bad3740fd1fb778f629a1fe866bb43b" +checksum = "35f021a55afd68ff2364ccfddaa364fc9a38a72200cdc74fcfb8dc3231d38f2c" dependencies = [ "alloy-eips", "alloy-primitives", @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5659581e41e8fe350ecc3593cb5c9dcffddfd550896390f2b78a07af67b0fa" +checksum = "5a0ecca7a71b1f88e63d19e2d9397ce56949d3dd3484fd73c73d0077dc5c93d4" dependencies = [ "alloy-consensus", "alloy-eips", @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944085cf3ac8f32d96299aa26c03db7c8ca6cdaafdbc467910b889f0328e6b70" +checksum = "dd26132cbfa6e5f191a01f7b9725eaa0680a953be1fd005d588b0e9422c16456" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35887da30b5fc50267109a3c61cd63e6ca1f45967983641053a40ee83468c1" +checksum = "7473a19f02b25f8e1e8c69d35f02c07245694d11bd91bfe00e9190ac106b3838" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "alloy-ens" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75154bf385a52fe9452d98bf2026c136543976ee11394b2ef28011005f390327" +checksum = "ea90fc9495e583d49b727586457676674089f2367cedf781222325562e4850d9" dependencies = [ "alloy-contract", "alloy-primitives", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d4009efea6f403b3a80531f9c6f70fc242399498ff71196a1688cc1c901f44" +checksum = "17b2c29f25098bfa4cd3d9ec7806e1506716931e188c7c0843284123831c2cf1" dependencies = [ "alloy-eips", "alloy-primitives", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883dee3b4020fcb5667ee627b4f401e899dad82bf37b246620339dd980720ed9" +checksum = "7a4d1f49fdf9780b60e52c20ffcc1e352d8d27885cc8890620eb584978265dd9" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6e5b8ac1654a05c224390008e43634a2bdc74e181e02cf8ed591d8b3d4ad08" +checksum = "2991c432e149babfd996194f8f558f85d7326ac4cf52c55732d32078ff0282d4" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7980333dd9391719756ac28bc2afa9baa705fc70ffd11dc86ab078dd64477" +checksum = "1d540d962ddbc3e95153bafe56ccefeb16dfbffa52c5f7bdd66cd29ec8f52259" dependencies = [ "alloy-consensus", "alloy-eips", @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478a42fe167057b7b919cd8b0c2844f0247f667473340dad100eaf969de5754e" +checksum = "7e96d8084a1cf96be2df6219ac407275ac20c1136fa01f911535eb489aa006e8" dependencies = [ "alloy-chains", "alloy-consensus", @@ -437,9 +437,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0a99b17987f40a066b29b6b56d75e84cd193b866cac27cae17b59f40338de95" +checksum = "8a682f14e10c3f4489c57b64ed457801b3e7ffc5091b6a35883d0e5960b9b894" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0c6d723fbdf4a87454e2e3a275e161be27edcfbf46e2e3255dd66c138634b6" +checksum = "194ff51cd1d2e65c66b98425e0ca7eb559ca1a579725834c986d84faf8e224c0" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -507,9 +507,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41492dac39365b86a954de86c47ec23dcc7452cdb2fde591caadc194b3e34c6" +checksum = "8d4fe522f6fc749c8afce721bdc8f73b715d317c3c02fcb9b51f7a143e4401dd" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -523,9 +523,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10493fa300a2757d8134f584800fef545c15905c95122bed1f6dde0b0d9dae27" +checksum = "c6af88d9714b499675164cac2fa2baadb3554579ab3ea8bc0d7b0c0de4f9d692" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -535,9 +535,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f7eb22670a972ad6c222a6c6dac3eef905579acffe9d63ab42be24c7d158535" +checksum = "124b742619519d5932e586631f11050028b29c30e3e195f2bb04228c886253d6" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -546,9 +546,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b6f0482c82310366ec3dcf4e5212242f256a69fcf1a26e5017e6704091ee95" +checksum = "1c6a6c8ae298c2739706ee3cd996c220b0ea406e6841a4e4290c7336edd5f811" dependencies = [ "alloy-primitives", "derive_more 2.0.1", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24c171377c0684e3860385f6d93fbfcc8ecc74f6cce8304c822bf1a50bacce0" +checksum = "9a1a77a23d609bca2e4a60f992dde5f987475cb064da355fa4dbd7cda2e1bb48" dependencies = [ "alloy-consensus", "alloy-eips", @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b777b98526bbe5b7892ca22a7fd5f18ed624ff664a79f40d0f9f2bf94ba79a84" +checksum = "781d4d5020bea8f020e164f5593101c2e2f790d66d04a0727839d03bc4411ed7" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a854af3fe8fce1cfe319fcf84ee8ba8cda352b14d3dd4221405b5fc6cce9e1" +checksum = "719e5eb9c15e21dab3dee2cac53505500e5e701f25d556734279c5f02154022a" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -610,9 +610,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc803e9b8d16154c856a738c376e002abe4b388e5fef91c8aebc8373e99fd45" +checksum = "37c751233a6067ccc8a4cbd469e0fd34e0d9475fd118959dbc777ae3af31bba7" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -622,9 +622,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8d2c52adebf3e6494976c8542fbdf12f10123b26e11ad56f77274c16a2a039" +checksum = "30be84f45d4f687b00efaba1e6290cbf53ccc8f6b8fbb54e4c2f9d2a0474ce95" dependencies = [ "alloy-primitives", "serde", @@ -633,9 +633,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c0494d1e0f802716480aabbe25549c7f6bc2a25ff33b08fd332bbb4b7d06894" +checksum = "fa8c24b883fe56395db64afcd665fca32dcdef670a59e5338de6892c2e38d7e9" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -650,9 +650,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0559495d87c099f7dbd0804145032e6a16ee675d1d2a15e98dc2658d64265cde" +checksum = "b806737bea3c5091982b8571f36d0ee324f0dcbaef7fedf6bbffbb63f04c5653" dependencies = [ "alloy-consensus", "alloy-network", @@ -668,9 +668,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946fbac85c43e1f73db388629e2115c41c4211ec8532bc46514d8153e81e818b" +checksum = "5ebe6b2f97da5e3033be4d2936ba01f3ea82b0573814cb14bf4778db3fde42f5" dependencies = [ "alloy-consensus", "alloy-network", @@ -686,9 +686,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4891d26fe418793186c30ea49451da8b5be2d9368547c9f1877002d3b0a192a" +checksum = "18d1c7a2c6d8d6532235b65fb40a298fe55df73311c212d368d48fb8ed9b03ce" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -706,9 +706,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c2435eb8979a020763ced3fb478932071c56e5f75ea86db41f320915d325ba" +checksum = "05724615fd2ec3417f5cd07cab908300cbb3aae5badc1b805ca70c555b26775f" dependencies = [ "alloy-consensus", "alloy-network", @@ -721,13 +721,14 @@ dependencies = [ "k256", "rand 0.8.5", "thiserror 2.0.15", + "zeroize", ] [[package]] name = "alloy-signer-trezor" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cca19612cf4d7a870f8cf42e2c86f7ba42979d009e5e947107f5e1a89d57706" +checksum = "eef56831eaf1b847b06b01be6e44fa9dbbb5a886014f780557ed5e26ca484374" dependencies = [ "alloy-consensus", "alloy-network", @@ -815,9 +816,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0107675e10c7f248bf7273c1e7fdb02409a717269cc744012e6f3c39959bfb" +checksum = "20b7f8b6c540b55e858f958d3a92223494cf83c4fb43ff9b26491edbeb3a3b71" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -839,9 +840,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e3736701b5433afd06eecff08f0688a71a10e0e1352e0bbf0bed72f0dd4e35" +checksum = "260e9584dfd7998760d7dfe1856c6f8f346462b9e7837287d7eddfb3922ef275" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -854,9 +855,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79064b5a08259581cb5614580010007c2df6deab1e8f3e8c7af8d7e9227008f" +checksum = "9491a1d81e97ae9d919da49e1c63dec4729c994e2715933968b8f780aa18793e" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -874,9 +875,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd607158cb9bc54cbcfcaab4c5f36c5b26994c7dc58b6f095ce27a54f270f3" +checksum = "d056ef079553e1f18834d6ef4c2793e4d51ac742021b2be5039dd623fe1354f0" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -892,9 +893,9 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962" +checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -908,9 +909,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6acb36318dfa50817154064fea7932adf2eec3f51c86680e2b37d7e8906c66bb" +checksum = "72e29436068f836727d4e7c819ae6bf6f9c9e19a32e96fc23e814709a277f23a" dependencies = [ "alloy-primitives", "darling 0.20.11", @@ -1835,9 +1836,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +checksum = "4fdbad9bd9dbcc6c5e68c311a841b54b70def3ca3b674c42fbebb265980539f8" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1852,6 +1853,7 @@ dependencies = [ "rustls-native-certs", "rustls-pki-types", "tokio", + "tokio-rustls", "tower", "tracing", ] @@ -1886,9 +1888,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.6" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" +checksum = "a3d57c8b53a72d15c8e190475743acf34e4996685e346a3448dd54ef696fc6e0" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -1910,9 +1912,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.7" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4" +checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -2717,9 +2719,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1" +checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394" dependencies = [ "clap", "log", @@ -3017,9 +3019,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e22e0ed40b96a48d3db274f72fd365bd78f67af39b6bbd47e8a15e1c6207ff" +checksum = "dccd746bf9b1038c0507b7cec21eb2b11222db96a2902c96e8c185d6d20fb9c4" dependencies = [ "cfg-if", "cpufeatures", @@ -5379,13 +5381,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http 1.3.1", "http-body 1.0.1", @@ -5393,6 +5396,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -7388,9 +7392,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn 2.0.106", @@ -8795,9 +8799,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "indexmap 2.10.0", "itoa", @@ -9832,9 +9836,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] From 6ebfaf322b87ee1341b641315db6b58291f0697d Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:40:31 +0200 Subject: [PATCH 41/55] chore: use get_or_calculate_hash better (#11350) --- crates/evm/coverage/src/inspector.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/crates/evm/coverage/src/inspector.rs b/crates/evm/coverage/src/inspector.rs index a5d142080252c..171b8a73498f9 100644 --- a/crates/evm/coverage/src/inspector.rs +++ b/crates/evm/coverage/src/inspector.rs @@ -39,7 +39,7 @@ where CTX: ContextTr, { fn initialize_interp(&mut self, interpreter: &mut Interpreter, _context: &mut CTX) { - get_or_insert_contract_hash(interpreter); + interpreter.bytecode.get_or_calculate_hash(); self.insert_map(interpreter); } @@ -61,7 +61,7 @@ impl LineCoverageCollector { /// See comments on `current_map` for more details. #[inline] fn get_or_insert_map(&mut self, interpreter: &mut Interpreter) -> &mut HitMap { - let hash = get_or_insert_contract_hash(interpreter); + let hash = interpreter.bytecode.get_or_calculate_hash(); if self.current_hash != *hash { self.insert_map(interpreter); } @@ -82,16 +82,3 @@ impl LineCoverageCollector { .into(); } } - -/// Helper function for extracting contract hash used to record coverage hit map. -/// -/// If the contract hash is zero (contract not yet created but it's going to be created in current -/// tx) then the hash is calculated from the bytecode. -#[inline] -fn get_or_insert_contract_hash(interpreter: &mut Interpreter) -> B256 { - interpreter - .bytecode - .hash() - .filter(|h| !h.is_zero()) - .unwrap_or_else(|| interpreter.bytecode.get_or_calculate_hash()) -} From cc09011f8951d3567af71f24241eb8e197f6dd95 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:54:22 +0530 Subject: [PATCH 42/55] resolve more conflicts --- crates/cheatcodes/src/evm.rs | 5 ++- crates/forge/tests/it/repros.rs | 3 ++ testdata/default/repros/Issue11353.t.sol | 40 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 testdata/default/repros/Issue11353.t.sol diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 16e388078e6a1..469cf6e927a16 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -8,7 +8,8 @@ use crate::{ use alloy_consensus::TxEnvelope; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_genesis::{Genesis, GenesisAccount}; -use alloy_primitives::{Address, B256, U256, hex, keccak256, map::HashMap}; +use alloy_network::eip2718::EIP4844_TX_TYPE_ID; +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}; @@ -564,6 +565,8 @@ impl Cheatcode for blobhashesCall { see EIP-4844: https://eips.ethereum.org/EIPS/eip-4844" ); ccx.ecx.tx.blob_hashes.clone_from(hashes); + // force this as 4844 txtype + ccx.ecx.tx.tx_type = EIP4844_TX_TYPE_ID; Ok(Default::default()) } } diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index b9ed5a2711916..5d1caf5048ca5 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -416,3 +416,6 @@ test_repro!(10586); // https://github.com/foundry-rs/foundry/issues/10957 test_repro!(10957); + +// https://github.com/foundry-rs/foundry/issues/11353 +test_repro!(11353); diff --git a/testdata/default/repros/Issue11353.t.sol b/testdata/default/repros/Issue11353.t.sol new file mode 100644 index 0000000000000..ae3305bcfc036 --- /dev/null +++ b/testdata/default/repros/Issue11353.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract Blobhash { + function getIndices(uint256[] calldata blobIndices) public view returns (bytes32[] memory) { + bytes32[] memory blobHashes = new bytes32[](blobIndices.length); + for (uint256 i = 0; i < blobIndices.length; i++) { + uint256 blobIndex = blobIndices[i]; + bytes32 blobHash = blobhash(blobIndex); + require(blobHash != 0, "blob not found"); + blobHashes[i] = blobHash; + } + return blobHashes; + } +} + +// https://github.com/foundry-rs/foundry/issues/11353 +contract Issue11353Test is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + Blobhash public blobhashContract; + + function setUp() public { + blobhashContract = new Blobhash(); + } + + function test_blobhashes() public { + uint256[] memory blobIndices = new uint256[](1); + blobIndices[0] = 0; + + bytes32[] memory blobHashes = new bytes32[](1); + blobHashes[0] = keccak256(abi.encode(0)); + vm.blobhashes(blobHashes); + + vm.assertEq(blobhashContract.getIndices(blobIndices), blobHashes); + } +} From 9c78c61e47b538e9df1bc751aaa34aaede1903f0 Mon Sep 17 00:00:00 2001 From: srdtrk <59252793+srdtrk@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:55:48 +0400 Subject: [PATCH 43/55] fix(lint): 'unwrapped-modifier-logic' incorrectly marked with `Severity::Gas` (#11358) fix(lint): 'unwrapped-modifier-logic' incorrectly marked with Severity::Gas --- crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs b/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs index e1237aaa0644c..85714780cda25 100644 --- a/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs +++ b/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs @@ -8,7 +8,7 @@ use solar_sema::hir::{self, Res}; declare_forge_lint!( UNWRAPPED_MODIFIER_LOGIC, - Severity::Gas, + Severity::CodeSize, "unwrapped-modifier-logic", "wrap modifier logic to reduce code size" ); From cbbbdb623c203c6c23956c6543ee38faf529dd66 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:46:16 +0530 Subject: [PATCH 44/55] feat: identify and decode nested structs --- crates/cheatcodes/src/evm.rs | 263 ++++++++-- crates/forge/tests/it/cheats.rs | 7 +- .../cheats/StateDiffStorageLayout.t.sol | 469 +++--------------- .../default/cheats/StateDiffStructTest.t.sol | 196 ++------ 4 files changed, 357 insertions(+), 578 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 469cf6e927a16..0f5c58c34f5b1 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -153,7 +153,7 @@ where if slot_type.label.starts_with("struct ") { slot_type.label.clone() } else { - format!("struct {}", name) + format!("struct {name}") } } _ => slot_type.label.clone(), @@ -1487,14 +1487,17 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap read bytes 16-32 (rightmost) + // offset 16, size 16 -> read bytes 0-16 (leftmost) + let byte_start = 32 - offset - size; prev_bytes[32 - size..].copy_from_slice( &storage_access.previousValue.0 - [byte_offset..byte_offset + size], + [byte_start..byte_start + size], ); new_bytes[32 - size..].copy_from_slice( &storage_access.newValue.0 - [byte_offset..byte_offset + size], + [byte_start..byte_start + size], ); } @@ -1717,8 +1720,8 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option>(members_value.clone()) { @@ -1726,9 +1729,17 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Option Option>(members_value.clone()) && !members.is_empty() { + trace!("Found multi-slot struct {} at slot {}", storage.label, slot_str); // Find the first member (at slot offset 0) if let Some(first_member) = members.iter().find(|m| m.slot == "0") { + trace!( + "First member: {} of type {}", + first_member.label, first_member.storage_type + ); if let Some(member_type_info) = storage_layout.types.get(&first_member.storage_type) - && let Ok(member_type) = DynSolType::parse(&member_type_info.label) { - // Return info for the first member instead of the struct - return Some(SlotInfo { - label: format!("{}.{}", storage.label, first_member.label), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: first_member.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); + trace!("First member type label: {}", member_type_info.label); + // Check if the first member is itself a multi-slot struct + if member_type_info.label.starts_with("struct ") + && let Some(inner_members_value) = member_type_info.other.get("members") + && let Ok(inner_members) = + serde_json::from_value::>(inner_members_value.clone()) + { + // Check if the inner struct is multi-slot + let is_inner_multi_slot = !inner_members.iter().all(|m| m.slot == "0"); + + if is_inner_multi_slot { + // The first member is a multi-slot struct, so decode its first + // member + if let Some(inner_first_member) = + inner_members.iter().find(|m| m.slot == "0") + && let Some(inner_first_type_info) = + storage_layout.types.get(&inner_first_member.storage_type) + && let Ok(inner_first_type) = + DynSolType::parse(&inner_first_type_info.label) + { + return Some(SlotInfo { + label: format!( + "{}.{}.{}", + storage.label, + first_member.label, + inner_first_member.label + ), + slot_type: SlotType { + label: inner_first_type_info.label.clone(), + dyn_sol_type: inner_first_type, + }, + offset: inner_first_member.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); + } + } else { + // The first member is a single-slot struct, return it with its + // label We can't parse struct types + // directly, so just return basic info + return Some(SlotInfo { + label: format!("{}.{}", storage.label, first_member.label), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: DynSolType::Tuple(vec![]), /* Placeholder + * for struct */ + }, + offset: first_member.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); + } + } else if !member_type_info.label.starts_with("struct ") { + // First member is not a struct, decode it normally + if let Ok(member_type) = DynSolType::parse(&member_type_info.label) { + return Some(SlotInfo { + label: format!("{}.{}", storage.label, first_member.label), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: first_member.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); + } + } } } } @@ -1881,8 +1953,8 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Option>(members_value.clone()) + { + // Check each member to see if it matches the current slot + for member in &members { + // Calculate the absolute slot for this member + let member_slot_offset = match U256::from_str(&member.slot) { + Ok(s) => s, + Err(_) => continue, + }; + let member_absolute_slot = base_slot + member_slot_offset; + + if member_absolute_slot == slot { + // Check if this member is itself a struct + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) { + // If the member is a struct, we need to check if it's multi-slot + if member_type_info.label.starts_with("struct ") + && let Some(member_members_value) = + member_type_info.other.get("members") + && let Ok(member_members) = + serde_json::from_value::>(member_members_value.clone()) + { + // Check if this is a multi-slot struct + let is_multi_slot = !member_members.iter().all(|m| m.slot == "0"); + + if is_multi_slot { + // For multi-slot nested structs, find the first member + if let Some(first_member) = + member_members.iter().find(|m| m.slot == "0") + && let Some(first_member_type_info) = + storage_layout.types.get(&first_member.storage_type) + && let Ok(first_member_type) = + DynSolType::parse(&first_member_type_info.label) + { + return Some(SlotInfo { + label: format!( + "{}.{}.{}", + storage.label, member.label, first_member.label + ), + slot_type: SlotType { + label: first_member_type_info.label.clone(), + dyn_sol_type: first_member_type, + }, + offset: first_member.offset, + slot: slot_str, + members: None, + decoded: None, + }); + } + } else { + // Single-slot nested struct - decode as a struct with members + if let Ok(member_type) = DynSolType::parse(&member_type_info.label) + { + return Some(SlotInfo { + label: format!("{}.{}", storage.label, member.label), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str, + members: None, + decoded: None, + }); + } + } + } else if let Ok(member_type) = DynSolType::parse(&member_type_info.label) { + // Regular member (not a struct) + return Some(SlotInfo { + label: format!("{}.{}", storage.label, member.label), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str, + members: None, + decoded: None, + }); + } + } + } + + // Also check if the slot belongs to a member that is itself a multi-slot struct + // We need to check the member's storage layout to see if it spans multiple slots + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && member_type_info.label.starts_with("struct ") + && let Some(member_members_value) = member_type_info.other.get("members") + && let Ok(member_members) = + serde_json::from_value::>(member_members_value.clone()) + { + // Check each sub-member of this struct member + for sub_member in &member_members { + let sub_member_slot_offset = match U256::from_str(&sub_member.slot) { + Ok(s) => s, + Err(_) => continue, + }; + let sub_member_absolute_slot = + base_slot + member_slot_offset + sub_member_slot_offset; + + if sub_member_absolute_slot == slot + && let Some(sub_member_type_info) = + storage_layout.types.get(&sub_member.storage_type) + && let Ok(sub_member_type) = + DynSolType::parse(&sub_member_type_info.label) + { + return Some(SlotInfo { + label: format!( + "{}.{}.{}", + storage.label, member.label, sub_member.label + ), + slot_type: SlotType { + label: sub_member_type_info.label.clone(), + dyn_sol_type: sub_member_type, + }, + offset: sub_member.offset, + slot: slot_str, + members: None, + decoded: None, + }); + } + } } } } @@ -1979,12 +2180,12 @@ fn decode_storage_value( // Extract bytes for this member from the full value let mut member_bytes = [0u8; 32]; if offset + size <= 32 { - // In Solidity storage, values are right-aligned - // For offset 0, we want the rightmost bytes - // For offset 16 (for a uint128), we want bytes 0-16 - let byte_offset = 32 - offset - size; + // For packed storage: offset 0 is at the rightmost position + // offset 0, size 16 -> read bytes 16-32 (rightmost) + // offset 16, size 16 -> read bytes 0-16 (leftmost) + let byte_start = 32 - offset - size; member_bytes[32 - size..] - .copy_from_slice(&value.0[byte_offset..byte_offset + size]); + .copy_from_slice(&value.0[byte_start..byte_start + size]); } // Decode the member value diff --git a/crates/forge/tests/it/cheats.rs b/crates/forge/tests/it/cheats.rs index daadd874b9593..2ad27e513871c 100644 --- a/crates/forge/tests/it/cheats.rs +++ b/crates/forge/tests/it/cheats.rs @@ -17,7 +17,7 @@ use foundry_test_utils::{Filter, init_tracing}; 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|StateDiffStorageLayoutTest)"); + .exclude_contracts("(Isolated|WithSeed|StateDiff)"); // Exclude FFI tests on Windows because no `echo`, and file tests that expect certain file paths if cfg!(windows) { @@ -38,7 +38,7 @@ 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}*")) - .exclude_contracts("StateDiffStorageLayoutTest"); + .exclude_contracts("StateDiff"); let runner = test_data.runner_with(|config| { config.isolate = true; @@ -76,8 +76,7 @@ async fn test_state_diff_storage_layout() { let output = get_compiled(&mut project); ForgeTestData { project, output, config: config.into(), profile } }; - let filter = - Filter::new(".*", "StateDiffStorageLayoutTest", &format!(".*cheats{RE_PATH_SEPARATOR}*")); + let filter = Filter::new(".*", "StateDiff", &format!(".*cheats{RE_PATH_SEPARATOR}*")); let runner = test_data.runner_with(|config| { config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index d8693c8dfa813..54e381c8df4ae 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -86,11 +86,7 @@ contract TwoDArrayStorage { matrix[i][j] = value; } - function setAddresses2D( - address[2] memory row0, - address[2] memory row1, - address[2] memory row2 - ) public { + function setAddresses2D(address[2] memory row0, address[2] memory row1, address[2] memory row2) public { addresses2D[0] = row0; addresses2D[1] = row1; addresses2D[2] = row2; @@ -155,86 +151,31 @@ 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(); - assertTrue( - accesses.length >= 3, - "Should have at least 3 account accesses for the calls" - ); + assertTrue(accesses.length >= 3, "Should have at least 3 account accesses for the calls"); // Verify storage accesses for SimpleStorage bool foundValueSlot = false; @@ -245,11 +186,7 @@ contract StateDiffStorageLayoutTest is DSTest { for (uint256 i = 0; i < accesses.length; i++) { if (accesses[i].account == address(simpleStorage)) { - for ( - uint256 j = 0; - j < accesses[i].storageAccesses.length; - j++ - ) { + 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; @@ -281,88 +218,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 @@ -381,36 +262,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(); } @@ -430,80 +287,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(); } @@ -526,60 +327,20 @@ 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(); } @@ -597,64 +358,24 @@ contract StateDiffStorageLayoutTest is DSTest { 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" - ); + 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" - assertContains( - stateDiff, - unicode"0 → 42", - "Should show decoded uint256 value" - ); + assertContains(stateDiff, unicode"0 → 42", "Should show decoded uint256 value"); // For addresses, should show decoded address format - assertContains( - stateDiff, - "0x000000000000000000000000000000000000bEEF", - "Should show decoded address" - ); + 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" - ); + 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(); } @@ -675,16 +396,8 @@ contract StateDiffStorageLayoutTest is DSTest { 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" - ); + assertContains(stateDiff, "@ 0x00 (value", "Slot 0 should be formatted as 0x00"); + assertContains(stateDiff, "@ 0x01 (owner", "Slot 1 should be formatted as 0x01"); vm.stopAndReturnStateDiff(); } @@ -704,39 +417,19 @@ contract StateDiffStorageLayoutTest is DSTest { 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" - ); + 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" - ); + 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 { + function assertContains(string memory haystack, string memory needle, string memory message) internal pure { bytes memory haystackBytes = bytes(haystack); bytes memory needleBytes = bytes(needle); @@ -745,11 +438,7 @@ contract StateDiffStorageLayoutTest is DSTest { } bool found = false; - for ( - uint256 i = 0; - i <= haystackBytes.length - needleBytes.length; - i++ - ) { + 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]) { diff --git a/testdata/default/cheats/StateDiffStructTest.t.sol b/testdata/default/cheats/StateDiffStructTest.t.sol index 8a61fda7a1017..8c856c3789465 100644 --- a/testdata/default/cheats/StateDiffStructTest.t.sol +++ b/testdata/default/cheats/StateDiffStructTest.t.sol @@ -5,6 +5,7 @@ import "ds-test/test.sol"; import "cheats/Vm.sol"; contract DiffTest { + // slot 0 struct TestStruct { uint128 a; uint128 b; @@ -12,16 +13,16 @@ contract DiffTest { // Multi-slot struct (spans 3 slots) struct MultiSlotStruct { - uint256 value1; // slot 0 - address addr; // slot 1 (takes 20 bytes, but uses full slot) - uint256 value2; // slot 2 + uint256 value1; // slot 1 + address addr; // slot 2 (takes 20 bytes, but uses full slot) + uint256 value2; // slot 3 } // Nested struct with MultiSlotStruct as inner struct NestedStruct { - MultiSlotStruct inner; // slots 0-2 (spans 3 slots) - uint256 value; // slot 3 - address owner; // slot 4 + MultiSlotStruct inner; // slots 4-6 (spans 3 slots) + uint256 value; // slot 7 + address owner; // slot 8 } TestStruct internal testStruct; @@ -39,13 +40,7 @@ contract DiffTest { multiSlotStruct.value2 = v2; } - function setNestedStruct( - uint256 v1, - address a, - uint256 v2, - uint256 v, - address o - ) public { + function setNestedStruct(uint256 v1, address a, uint256 v2, uint256 v, address o) public { nestedStruct.inner.value1 = v1; nestedStruct.inner.addr = a; nestedStruct.inner.value2 = v2; @@ -72,45 +67,25 @@ contract StateDiffStructTest is DSTest { // Get the state diff as JSON string memory stateDiffJson = vm.getStateDiffJson(); + // Debug: log the JSON for inspection + emit log_string("State diff JSON (testdata):"); + emit log_string(stateDiffJson); + // Check that the struct is properly labeled - assertContains( - stateDiffJson, - '"label":"testStruct"', - "Should contain 'testStruct' label" - ); + assertContains(stateDiffJson, '"label":"testStruct"', "Should contain 'testStruct' label"); // Check that the type is correctly identified as a struct - assertContains( - stateDiffJson, - '"type":"struct DiffTest.TestStruct"', - "Should contain struct type" - ); + assertContains(stateDiffJson, '"type":"struct DiffTest.TestStruct"', "Should contain struct type"); // Check for members field - structs have members with individual decoded values - assertContains( - stateDiffJson, - '"members":', - "Should contain members field for struct" - ); + assertContains(stateDiffJson, '"members":', "Should contain members field for struct"); // Check that member 'a' is properly decoded - assertContains( - stateDiffJson, - '"label":"a"', - "Should contain member 'a' label" - ); - assertContains( - stateDiffJson, - '"type":"uint128"', - "Should contain uint128 type for members" - ); + assertContains(stateDiffJson, '"label":"a"', "Should contain member 'a' label"); + assertContains(stateDiffJson, '"type":"uint128"', "Should contain uint128 type for members"); // Check that member 'b' is properly decoded - assertContains( - stateDiffJson, - '"label":"b"', - "Should contain member 'b' label" - ); + assertContains(stateDiffJson, '"label":"b"', "Should contain member 'b' label"); // The members should have decoded values // Check specific decoded values for each member in the members array @@ -147,28 +122,15 @@ contract StateDiffStructTest is DSTest { bool foundStructAccess = false; for (uint256 i = 0; i < accesses.length; i++) { if (accesses[i].account == address(test)) { - for ( - uint256 j = 0; - j < accesses[i].storageAccesses.length; - j++ - ) { - Vm.StorageAccess memory access = accesses[i] - .storageAccesses[j]; + for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + Vm.StorageAccess memory access = accesses[i].storageAccesses[j]; if (access.slot == bytes32(uint256(0)) && access.isWrite) { foundStructAccess = true; // Verify the storage values - assertEq( - access.previousValue, - bytes32(uint256(0)), - "Previous value should be 0" - ); + assertEq(access.previousValue, bytes32(uint256(0)), "Previous value should be 0"); assertEq( access.newValue, - bytes32( - uint256( - 0x0000000000000000000200000000000000000000000000000001 - ) - ), + bytes32(uint256(0x0000000000000000000200000000000000000000000000000001)), "New value should pack a=1 and b=2" ); } @@ -176,10 +138,7 @@ contract StateDiffStructTest is DSTest { } } - assertTrue( - foundStructAccess, - "Should have found struct storage access" - ); + assertTrue(foundStructAccess, "Should have found struct storage access"); } function testMultiSlotStruct() public { @@ -202,9 +161,7 @@ contract StateDiffStructTest is DSTest { // Check that the struct's first member is properly labeled assertContains( - stateDiffJson, - '"label":"multiSlotStruct.value1"', - "Should contain 'multiSlotStruct.value1' label" + stateDiffJson, '"label":"multiSlotStruct.value1"', "Should contain 'multiSlotStruct.value1' label" ); // For multi-slot structs, the base slot now shows the first member's type @@ -220,28 +177,12 @@ contract StateDiffStructTest is DSTest { '"label":"multiSlotStruct.value1"', "Should contain multiSlotStruct.value1 label for first slot" ); - assertContains( - stateDiffJson, - '"label":"multiSlotStruct.addr"', - "Should contain member 'addr' label" - ); - assertContains( - stateDiffJson, - '"label":"multiSlotStruct.value2"', - "Should contain member 'value2' label" - ); + assertContains(stateDiffJson, '"label":"multiSlotStruct.addr"', "Should contain member 'addr' label"); + assertContains(stateDiffJson, '"label":"multiSlotStruct.value2"', "Should contain member 'value2' label"); // Check member types - assertContains( - stateDiffJson, - '"type":"uint256"', - "Should contain uint256 type" - ); - assertContains( - stateDiffJson, - '"type":"address"', - "Should contain address type" - ); + assertContains(stateDiffJson, '"type":"uint256"', "Should contain uint256 type"); + assertContains(stateDiffJson, '"type":"address"', "Should contain address type"); // Check that value1 is properly decoded from slot 1 assertContains( @@ -249,7 +190,7 @@ contract StateDiffStructTest is DSTest { '"decoded":{"previousValue":"0","newValue":"123456789"}', "value1 should be decoded from slot 1" ); - + // Also verify the raw hex value assertContains( stateDiffJson, @@ -291,87 +232,44 @@ contract StateDiffStructTest is DSTest { // Get the state diff as JSON string memory stateDiffJson = vm.getStateDiffJson(); - // Check that the struct is properly labeled - assertContains( - stateDiffJson, - '"label":"nestedStruct"', - "Should contain 'nestedStruct' label" - ); + // Debug: log the JSON for inspection + emit log_string("State diff JSON (testdata):"); + emit log_string(stateDiffJson); - // Check that the type is correctly identified as a struct assertContains( stateDiffJson, - '"type":"struct DiffTest.NestedStruct"', - "Should contain struct type" + '"decoded":{"previousValue":"0","newValue":"111111111"},"label":"nestedStruct.inner.value1"', + "Should decode inner.value1 correctly" ); - // Nested struct with multi-slot inner struct doesn't have members field either - // Each member appears as a separate slot - - // Check that nested struct labels are properly set assertContains( stateDiffJson, - '"label":"nestedStruct"', - "Should contain nestedStruct label" + '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000cafE"},"label":"nestedStruct.inner.addr"', + "Should decode inner.addr correctly" ); - // Check other members have proper labels assertContains( stateDiffJson, - '"label":"nestedStruct.value"', - "Should contain member 'value' label" - ); - assertContains( - stateDiffJson, - '"label":"nestedStruct.owner"', - "Should contain member 'owner' label" + '"decoded":{"previousValue":"0","newValue":"222222222"},"label":"nestedStruct.inner.value2"', + "Should decode inner.value2 correctly" ); - // The inner struct members are in slots 4, 5, 6 but we only see their storage diffs - // They don't appear with member labels in this test since they're part of the nested struct - - // Check that slot 4 has the first value assertContains( stateDiffJson, "0x00000000000000000000000000000000000000000000000000000000069f6bc7", "Slot 4 should contain inner.value1 in hex" ); - // Note: addresses in slots 5 and 6 may not have labels due to nested struct complexity - // But the important values are decoded correctly - // Check decoded values for outer struct members - // Slot 7 should have nestedStruct.value decoded with previous=0 and new=333333333 - assertContains( - stateDiffJson, - '"label":"nestedStruct.value"', - "Should have nestedStruct.value label" - ); assertContains( stateDiffJson, - '"slot":"7"', - "nestedStruct.value should be in slot 7" - ); - assertContains( - stateDiffJson, - '"previousValue":"0","newValue":"333333333"', + '"decoded":{"previousValue":"0","newValue":"333333333"},"label":"nestedStruct.value"', "Should decode nestedStruct.value correctly" ); - // Slot 8 should have nestedStruct.owner decoded - assertContains( - stateDiffJson, - '"label":"nestedStruct.owner"', - "Should have nestedStruct.owner label" - ); - assertContains( - stateDiffJson, - '"slot":"8"', - "nestedStruct.owner should be in slot 8" - ); assertContains( stateDiffJson, - '"newValue":"0x000000000000000000000000000000000000bEEF"', - "Should decode owner address correctly" + '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000bEEF"},"label":"nestedStruct.owner"', + "Should decode nestedStruct.owner correctly" ); // Stop recording @@ -379,11 +277,7 @@ contract StateDiffStructTest is DSTest { } // Helper function to check if a string contains a substring - function assertContains( - string memory haystack, - string memory needle, - string memory message - ) internal pure { + function assertContains(string memory haystack, string memory needle, string memory message) internal pure { bytes memory haystackBytes = bytes(haystack); bytes memory needleBytes = bytes(needle); @@ -392,11 +286,7 @@ contract StateDiffStructTest is DSTest { } bool found = false; - for ( - uint256 i = 0; - i <= haystackBytes.length - needleBytes.length; - i++ - ) { + 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]) { From 55249e883c373e00de48146df9f4fe6752cb451d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:04:41 +0530 Subject: [PATCH 45/55] cleanup --- crates/cheatcodes/src/evm.rs | 879 +++++++++++++++-------------------- 1 file changed, 365 insertions(+), 514 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 2b6a0be4c8d0d..47a8d7faf10f7 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -254,7 +254,7 @@ impl Display for AccountStateDiffs { f, "@ {slot} ({}, {}): {} → {}", slot_info.label, - slot_info.dyn_sol_type, + slot_info.slot_type.dyn_sol_type, format_dyn_sol_value_raw(&decoded.previous_value), format_dyn_sol_value_raw(&decoded.new_value) )?; @@ -265,7 +265,7 @@ impl Display for AccountStateDiffs { f, "@ {slot} ({}, {}): {} → {}", slot_info.label, - slot_info.dyn_sol_type, + slot_info.slot_type.dyn_sol_type, slot_changes.previous_value, slot_changes.new_value )?; @@ -1442,122 +1442,115 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap { // Get storage layout info for this slot - let slot_info = storage_layouts - .get(&storage_access.account) - .and_then(|layout| { - trace!("Getting slot info for slot {:?} in layout with {} items", - storage_access.slot, layout.storage.len()); - let info = get_slot_info(layout, &storage_access.slot); - if info.is_some() { - trace!("Found slot info for slot {:?}", storage_access.slot); - } else { - trace!("No slot info found for slot {:?}", storage_access.slot); - } - info - }); + let slot_info = layout + .and_then(|layout| get_slot_info(layout, &storage_access.slot)); // Try to decode values if we have slot info - let (decoded, slot_info_with_decoded) = - if let Some(mut info) = slot_info { - // Check if this is a struct with members - if let Some(ref mut members) = info.members { - // Decode each member individually - for member in members.iter_mut() { - let offset = member.offset as usize; - let size = match &member.slot_type.dyn_sol_type { - DynSolType::Uint(bits) - | DynSolType::Int(bits) => bits / 8, - DynSolType::Address => 20, - DynSolType::Bool => 1, - DynSolType::FixedBytes(size) => *size, - _ => 32, // Default to full word - }; - - // Extract and decode member values - let mut prev_bytes = [0u8; 32]; - let mut new_bytes = [0u8; 32]; - - if offset + size <= 32 { - // In Solidity storage, values are right-aligned - // For offset 0, we want the rightmost bytes - // For offset 16 (for a uint128), we want bytes - // 0-16 - // For packed storage: offset 0 is at the rightmost position - // offset 0, size 16 -> read bytes 16-32 (rightmost) - // offset 16, size 16 -> read bytes 0-16 (leftmost) - let byte_start = 32 - offset - size; - prev_bytes[32 - size..].copy_from_slice( - &storage_access.previousValue.0 - [byte_start..byte_start + size], - ); - new_bytes[32 - size..].copy_from_slice( - &storage_access.newValue.0 - [byte_start..byte_start + size], - ); + let (decoded, slot_info_with_decoded) = if let Some(mut info) = + slot_info + { + // Check if this is a struct with members + if let Some(ref mut members) = info.members { + // Decode each member individually + for member in members.iter_mut() { + let offset = member.offset as usize; + let size = match &member.slot_type.dyn_sol_type { + DynSolType::Uint(bits) | DynSolType::Int(bits) => { + bits / 8 } + DynSolType::Address => 20, + DynSolType::Bool => 1, + DynSolType::FixedBytes(size) => *size, + _ => 32, // Default to full word + }; - // Decode the member values - if let (Ok(prev_val), Ok(new_val)) = ( - member - .slot_type - .dyn_sol_type - .abi_decode(&prev_bytes), - member - .slot_type - .dyn_sol_type - .abi_decode(&new_bytes), - ) { - member.decoded = Some(DecodedSlotValues { - previous_value: prev_val, - new_value: new_val, - }); - } + // Extract and decode member values + let mut prev_bytes = [0u8; 32]; + let mut new_bytes = [0u8; 32]; + + if offset + size <= 32 { + // In Solidity storage, values are right-aligned + // For offset 0, we want the rightmost bytes + // For offset 16 (for a uint128), we want bytes + // 0-16 + // For packed storage: offset 0 is at the rightmost + // position + // offset 0, size 16 -> read bytes 16-32 (rightmost) + // offset 16, size 16 -> read bytes 0-16 (leftmost) + let byte_start = 32 - offset - size; + prev_bytes[32 - size..].copy_from_slice( + &storage_access.previousValue.0 + [byte_start..byte_start + size], + ); + new_bytes[32 - size..].copy_from_slice( + &storage_access.newValue.0 + [byte_start..byte_start + size], + ); } - // For structs with members, we don't need a top-level - // decoded value - (None, Some(info)) - } else { - // Not a struct, decode as a single value - let storage_layout = - storage_layouts.get(&storage_access.account); - let storage_type = storage_layout.and_then(|layout| { - layout - .storage - .iter() - .find(|s| s.slot == info.slot) - .and_then(|s| layout.types.get(&s.storage_type)) - }); - - let decoded = if let (Some(prev), Some(new)) = ( - decode_storage_value( - storage_access.previousValue, - &info.slot_type.dyn_sol_type, - storage_type, - storage_layout.as_ref().map(|arc| arc.as_ref()), - ), - decode_storage_value( - storage_access.newValue, - &info.slot_type.dyn_sol_type, - storage_type, - storage_layout.as_ref().map(|arc| arc.as_ref()), - ), + + // Decode the member values + if let (Ok(prev_val), Ok(new_val)) = ( + member + .slot_type + .dyn_sol_type + .abi_decode(&prev_bytes), + member + .slot_type + .dyn_sol_type + .abi_decode(&new_bytes), ) { - Some(DecodedSlotValues { - previous_value: prev, - new_value: new, - }) - } else { - None - }; - (decoded, Some(info)) + member.decoded = Some(DecodedSlotValues { + previous_value: prev_val, + new_value: new_val, + }); + } } + // For structs with members, we don't need a top-level + // decoded value + (None, Some(info)) } else { - (None, None) - }; + // Not a struct, decode as a single value + let storage_layout = + storage_layouts.get(&storage_access.account); + let storage_type = storage_layout.and_then(|layout| { + layout + .storage + .iter() + .find(|s| s.slot == info.slot) + .and_then(|s| layout.types.get(&s.storage_type)) + }); + + let decoded = if let (Some(prev), Some(new)) = ( + decode_storage_value( + storage_access.previousValue, + &info.slot_type.dyn_sol_type, + storage_type, + storage_layout.as_ref().map(|arc| arc.as_ref()), + ), + decode_storage_value( + storage_access.newValue, + &info.slot_type.dyn_sol_type, + storage_type, + storage_layout.as_ref().map(|arc| arc.as_ref()), + ), + ) { + Some(DecodedSlotValues { + previous_value: prev, + new_value: new, + }) + } else { + None + }; + (decoded, Some(info)) + } + } else { + (None, None) + }; slot_state_diff.insert(SlotStateDiff { previous_value: storage_access.previousValue, @@ -1573,10 +1566,8 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap BTreeMap( artifacts.find_by_deployed_code(&code_bytes) } -/// Gets storage layout info for a specific slot. -fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { - let slot = U256::from_be_bytes(slot.0); - let slot_str = slot.to_string(); - trace!("Looking for slot {} in storage layout", slot_str); +/// Helper function to parse struct members from storage type. +fn parse_struct_members(storage_type: &StorageType) -> Option> { + storage_type + .other + .get("members") + .and_then(|v| serde_json::from_value::>(v.clone()).ok()) +} - for storage in &storage_layout.storage { - trace!( - "Checking storage item: label={}, slot={}, type={}", - storage.label, storage.slot, storage.storage_type - ); - let base_slot = match U256::from_str(&storage.slot) { - Ok(s) => s, - Err(e) => { - trace!("Failed to parse slot '{}': {:?}", storage.slot, e); - continue; - } - }; - let storage_type = match storage_layout.types.get(&storage.storage_type) { - Some(t) => t, - None => { - trace!("Storage type '{}' not found in types", storage.storage_type); - continue; - } - }; +/// Helper function to check if a struct spans multiple slots. +fn is_multi_slot_struct(members: &[Storage]) -> bool { + !members.iter().all(|m| m.slot == "0") +} - // Parse the type - structs need special handling - let mut parsed_type = if storage_type.label.starts_with("struct ") { - // For structs, we need to build the type from members - if let Some(members_value) = storage_type.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - { - let mut member_types = Vec::new(); - for member in &members { - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) - && let Ok(member_type) = DynSolType::parse(&member_type_info.label) - { - member_types.push(member_type); - } else { - // If we can't parse all members, skip this item - continue; +/// Helper function to parse a type label into DynSolType. +fn parse_type_label(label: &str) -> Option { + if label.starts_with("struct ") { + // For struct types, we return a Tuple as placeholder + // since we handle structs specially + Some(DynSolType::Tuple(vec![])) + } else { + DynSolType::parse(label).ok() + } +} + +/// Recursively finds the first non-struct member of a multi-slot struct. +/// Returns the label path and slot info for the first primitive member. +fn find_first_primitive_member( + storage_layout: &StorageLayout, + base_label: &str, + members: &[Storage], + slot_str: &str, +) -> Option { + let first_member = members.iter().find(|m| m.slot == "0")?; + let member_type_info = storage_layout.types.get(&first_member.storage_type)?; + let label_path = format!("{}.{}", base_label, first_member.label); + + // If the first member is itself a multi-slot struct, recurse + if member_type_info.label.starts_with("struct ") { + if let Some(inner_members) = parse_struct_members(member_type_info) { + if is_multi_slot_struct(&inner_members) { + return find_first_primitive_member( + storage_layout, + &label_path, + &inner_members, + slot_str, + ); + } + } + } + + // First member is a primitive or single-slot struct + let member_type = parse_type_label(&member_type_info.label)?; + Some(SlotInfo { + label: label_path, + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: first_member.offset, + slot: slot_str.to_string(), + decoded: None, + members: None, + }) +} + +/// Recursively searches for a member at the requested slot within nested structs. +fn find_member_at_slot( + storage_layout: &StorageLayout, + base_label: &str, + members: &[Storage], + base_slot: U256, + requested_slot: U256, + slot_str: &str, + depth: usize, +) -> Option { + // Limit recursion depth to prevent stack overflow + const MAX_DEPTH: usize = 10; + if depth > MAX_DEPTH { + return None; + } + + for member in members { + let member_slot_offset = U256::from_str(&member.slot).ok()?; + let member_slot = base_slot + member_slot_offset; + let member_type_info = storage_layout.types.get(&member.storage_type)?; + let member_label = format!("{}.{}", base_label, member.label); + + if member_slot == requested_slot { + // Found the member at this slot + + // If it's a multi-slot struct, return its first primitive member + if member_type_info.label.starts_with("struct ") { + if let Some(nested_members) = parse_struct_members(member_type_info) { + if is_multi_slot_struct(&nested_members) { + return find_first_primitive_member( + storage_layout, + &member_label, + &nested_members, + slot_str, + ); } } - // Create a tuple type for now, will convert to CustomStruct below - DynSolType::Tuple(member_types) - } else { - trace!("Failed to get members for struct type '{}'", storage_type.label); - continue; } - } else { - // Non-struct types can be parsed directly - match DynSolType::parse(&storage_type.label) { - Ok(t) => t, - Err(e) => { - trace!("Failed to parse type label '{}': {:?}", storage_type.label, e); - continue; + + // Regular member or single-slot struct + let member_type = parse_type_label(&member_type_info.label)?; + return Some(SlotInfo { + label: member_label, + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str.to_string(), + members: None, + decoded: None, + }); + } + + // If this member is a struct, recursively search its members + if member_type_info.label.starts_with("struct ") { + if let Some(nested_members) = parse_struct_members(member_type_info) { + if let Some(slot_info) = find_member_at_slot( + storage_layout, + &member_label, + &nested_members, + member_slot, + requested_slot, + slot_str, + depth + 1, + ) { + return Some(slot_info); } } - }; - - // If it's a struct, enhance it with CustomStruct variant - if storage_type.label.starts_with("struct ") - && let Some(struct_name) = storage_type.label.strip_prefix("struct ") - && let Some(members_value) = storage_type.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - { - let mut prop_names = Vec::new(); - let mut member_types = Vec::new(); + } + } + + None +} +/// Handles struct slot decoding for both direct and member slot access. +fn handle_struct( + storage: &Storage, + storage_type: &StorageType, + storage_layout: &StorageLayout, + requested_slot: U256, + slot_str: &str, +) -> Option { + let members = parse_struct_members(storage_type)?; + let base_slot = U256::from_str(&storage.slot).ok()?; + + // Check if this is a direct slot match (accessing the struct's base slot) + if base_slot == requested_slot { + // Check if it's a multi-slot struct + if is_multi_slot_struct(&members) { + // For multi-slot structs at base slot, recursively find the first primitive member + return find_first_primitive_member(storage_layout, &storage.label, &members, slot_str); + } else { + // Single-slot struct - build member info + let mut member_infos = Vec::new(); for member in &members { - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) { - // For struct members, we need special handling - let member_type = if member_type_info.label.starts_with("struct ") { - // For nested structs, create a placeholder Tuple type - // The actual struct will be handled when its slot is accessed - DynSolType::Tuple(vec![]) - } else if let Ok(parsed) = DynSolType::parse(&member_type_info.label) { - parsed - } else { - continue; - }; - prop_names.push(member.label.clone()); - member_types.push(member_type); + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && let Some(member_type) = parse_type_label(&member_type_info.label) + { + member_infos.push(SlotInfo { + label: member.label.clone(), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); } } - // Only create CustomStruct if we successfully parsed all members - if member_types.len() == members.len() { - parsed_type = DynSolType::CustomStruct { - name: struct_name.to_string(), - prop_names, - tuple: member_types, - }; - } + // Build the CustomStruct type + let struct_name = + storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label); + let prop_names: Vec = members.iter().map(|m| m.label.clone()).collect(); + let member_types: Vec = + member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect(); + + let parsed_type = DynSolType::CustomStruct { + name: struct_name.to_string(), + prop_names, + tuple: member_types, + }; + + return Some(SlotInfo { + label: storage.label.clone(), + slot_type: SlotType { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset: storage.offset, + slot: slot_str.to_string(), + decoded: None, + members: if member_infos.is_empty() { None } else { Some(member_infos) }, + }); } + } - trace!("Parsed type '{}' for slot {} as: {:?}", storage_type.label, slot_str, parsed_type); + // Not a direct match - recursively search for the member at the requested slot + find_member_at_slot( + storage_layout, + &storage.label, + &members, + base_slot, + requested_slot, + slot_str, + 0, // Start with depth 0 + ) +} - // Build struct members info if this is a struct with members - // Only include members for single-slot structs (where all members are in the same slot) - let struct_members = if matches!(parsed_type, DynSolType::CustomStruct { .. }) - && let Some(members_value) = storage_type.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - { - // Check if all members are in the same slot (single-slot struct) - let all_same_slot = members.iter().all(|m| m.slot == "0"); - - if all_same_slot { - // Single-slot struct - include member info for decoding - let mut member_infos = Vec::new(); - - for member in &members { - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) - && let Ok(member_type) = DynSolType::parse(&member_type_info.label) - { - member_infos.push(SlotInfo { - label: member.label.clone(), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - } +/// Gets storage layout info for a specific slot. +fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { + let slot = U256::from_be_bytes(slot.0); + let slot_str = slot.to_string(); - if !member_infos.is_empty() { Some(member_infos) } else { None } - } else { - // Multi-slot struct - don't include members here - // Each member will be handled as a separate slot access - None - } - } else { - None - }; + for storage in &storage_layout.storage { + trace!( + "Checking storage item: label={}, slot={}, type={}", + storage.label, storage.slot, storage.storage_type + ); - // Check for exact slot match - if storage.slot == slot_str { - // For multi-slot structs at base slot, decode the first member - if matches!(parsed_type, DynSolType::CustomStruct { .. }) - && struct_members.is_none() // This is a multi-slot struct - && let Some(members_value) = storage_type.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - && !members.is_empty() - { - trace!("Found multi-slot struct {} at slot {}", storage.label, slot_str); - // Find the first member (at slot offset 0) - if let Some(first_member) = members.iter().find(|m| m.slot == "0") { - trace!( - "First member: {} of type {}", - first_member.label, first_member.storage_type - ); - if let Some(member_type_info) = - storage_layout.types.get(&first_member.storage_type) - { - trace!("First member type label: {}", member_type_info.label); - // Check if the first member is itself a multi-slot struct - if member_type_info.label.starts_with("struct ") - && let Some(inner_members_value) = member_type_info.other.get("members") - && let Ok(inner_members) = - serde_json::from_value::>(inner_members_value.clone()) - { - // Check if the inner struct is multi-slot - let is_inner_multi_slot = !inner_members.iter().all(|m| m.slot == "0"); - - if is_inner_multi_slot { - // The first member is a multi-slot struct, so decode its first - // member - if let Some(inner_first_member) = - inner_members.iter().find(|m| m.slot == "0") - && let Some(inner_first_type_info) = - storage_layout.types.get(&inner_first_member.storage_type) - && let Ok(inner_first_type) = - DynSolType::parse(&inner_first_type_info.label) - { - return Some(SlotInfo { - label: format!( - "{}.{}.{}", - storage.label, - first_member.label, - inner_first_member.label - ), - slot_type: SlotType { - label: inner_first_type_info.label.clone(), - dyn_sol_type: inner_first_type, - }, - offset: inner_first_member.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - } else { - // The first member is a single-slot struct, return it with its - // label We can't parse struct types - // directly, so just return basic info - return Some(SlotInfo { - label: format!("{}.{}", storage.label, first_member.label), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: DynSolType::Tuple(vec![]), /* Placeholder - * for struct */ - }, - offset: first_member.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - } else if !member_type_info.label.starts_with("struct ") { - // First member is not a struct, decode it normally - if let Ok(member_type) = DynSolType::parse(&member_type_info.label) { - return Some(SlotInfo { - label: format!("{}.{}", storage.label, first_member.label), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: first_member.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - } - } - } - } + let storage_type = storage_layout.types.get(&storage.storage_type)?; + let dyn_type = DynSolType::parse(&storage_type.label).ok(); - let label = match &parsed_type { - DynSolType::FixedArray(_, _) => { - // For arrays, label the base slot with indices - format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) - } - _ => storage.label.clone(), + // Check if we're able to match on a slot from the layout + if storage.slot == slot_str + && let Some(parsed_type) = dyn_type + { + // Successfully parsed - handle arrays or simple types + let label = if let DynSolType::FixedArray(_, _) = &parsed_type { + format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) + } else { + storage.label.clone() }; return Some(SlotInfo { @@ -1906,200 +1899,58 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option() + // Handle the case where the accessed `slot` if maybe different from the base slot. + let base_slot = U256::from_str(&storage.slot).ok()?; + + if let Some(parsed_type) = dyn_type + && let DynSolType::FixedArray(_, _) = parsed_type { - 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, &parsed_type, index); - - return Some(SlotInfo { - label, - slot_type: SlotType { - label: storage_type.label.clone(), - dyn_sol_type: parsed_type, - }, - offset: 0, - slot: slot.to_string(), - members: None, // Arrays don't have members like structs - decoded: None, // Will be populated when decoding - }); + if let Some(slot_info) = handle_array_slot(storage, storage_type, slot, base_slot, &slot_str) { + return Some(slot_info); } } - // Check if this slot is part of a multi-slot struct - // For structs that span multiple slots, check if the requested slot - // matches a member's absolute slot position - if storage_type.label.starts_with("struct ") - && let Some(members_value) = storage_type.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - { - for member in &members { - // Calculate absolute slot for this member - let member_slot = if let Ok(member_slot_offset) = U256::from_str(&member.slot) { - base_slot + member_slot_offset - } else { - continue; - }; - - // Check if this is the slot we're looking for - if member_slot == slot - && let Some(member_type_info) = storage_layout.types.get(&member.storage_type) - && let Ok(member_type) = DynSolType::parse(&member_type_info.label) - { - return Some(SlotInfo { - label: format!("{}.{}", storage.label, member.label), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: slot_str, - members: None, - decoded: None, - }); - } + // If type parsing fails and the label is a struct + if storage_type.label.starts_with("struct ") { + if let Some(slot_info) = handle_struct(storage, storage_type, storage_layout, slot, &slot_str) { + return Some(slot_info); } } + } - // Check if this slot belongs to a multi-slot struct or nested struct - // For structs with members at different slots, we need to check if the current slot - // matches one of the member slots - if let DynSolType::CustomStruct { .. } = &parsed_type - && let Some(members_value) = storage_type.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - { - // Check each member to see if it matches the current slot - for member in &members { - // Calculate the absolute slot for this member - let member_slot_offset = match U256::from_str(&member.slot) { - Ok(s) => s, - Err(_) => continue, - }; - let member_absolute_slot = base_slot + member_slot_offset; - - if member_absolute_slot == slot { - // Check if this member is itself a struct - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) { - // If the member is a struct, we need to check if it's multi-slot - if member_type_info.label.starts_with("struct ") - && let Some(member_members_value) = - member_type_info.other.get("members") - && let Ok(member_members) = - serde_json::from_value::>(member_members_value.clone()) - { - // Check if this is a multi-slot struct - let is_multi_slot = !member_members.iter().all(|m| m.slot == "0"); - - if is_multi_slot { - // For multi-slot nested structs, find the first member - if let Some(first_member) = - member_members.iter().find(|m| m.slot == "0") - && let Some(first_member_type_info) = - storage_layout.types.get(&first_member.storage_type) - && let Ok(first_member_type) = - DynSolType::parse(&first_member_type_info.label) - { - return Some(SlotInfo { - label: format!( - "{}.{}.{}", - storage.label, member.label, first_member.label - ), - slot_type: SlotType { - label: first_member_type_info.label.clone(), - dyn_sol_type: first_member_type, - }, - offset: first_member.offset, - slot: slot_str, - members: None, - decoded: None, - }); - } - } else { - // Single-slot nested struct - decode as a struct with members - if let Ok(member_type) = DynSolType::parse(&member_type_info.label) - { - return Some(SlotInfo { - label: format!("{}.{}", storage.label, member.label), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: slot_str, - members: None, - decoded: None, - }); - } - } - } else if let Ok(member_type) = DynSolType::parse(&member_type_info.label) { - // Regular member (not a struct) - return Some(SlotInfo { - label: format!("{}.{}", storage.label, member.label), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: slot_str, - members: None, - decoded: None, - }); - } - } - } + None +} - // Also check if the slot belongs to a member that is itself a multi-slot struct - // We need to check the member's storage layout to see if it spans multiple slots - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) - && member_type_info.label.starts_with("struct ") - && let Some(member_members_value) = member_type_info.other.get("members") - && let Ok(member_members) = - serde_json::from_value::>(member_members_value.clone()) - { - // Check each sub-member of this struct member - for sub_member in &member_members { - let sub_member_slot_offset = match U256::from_str(&sub_member.slot) { - Ok(s) => s, - Err(_) => continue, - }; - let sub_member_absolute_slot = - base_slot + member_slot_offset + sub_member_slot_offset; - - if sub_member_absolute_slot == slot - && let Some(sub_member_type_info) = - storage_layout.types.get(&sub_member.storage_type) - && let Ok(sub_member_type) = - DynSolType::parse(&sub_member_type_info.label) - { - return Some(SlotInfo { - label: format!( - "{}.{}.{}", - storage.label, member.label, sub_member.label - ), - slot_type: SlotType { - label: sub_member_type_info.label.clone(), - dyn_sol_type: sub_member_type, - }, - offset: sub_member.offset, - slot: slot_str, - members: None, - decoded: None, - }); - } - } - } - } - } +/// Handles array slot access. +fn handle_array_slot( + storage: &Storage, + storage_type: &StorageType, + slot: U256, + base_slot: U256, + slot_str: &str, +) -> Option { + // Check if slot is within array bounds + let total_bytes = storage_type.number_of_bytes.parse::().ok()?; + let total_slots = total_bytes.div_ceil(32); + + if slot >= base_slot && slot < base_slot + U256::from(total_slots) { + let parsed_type = DynSolType::parse(&storage_type.label).ok()?; + let index = (slot - base_slot).to::(); + let label = format_array_element_label(&storage.label, &parsed_type, index); + + return Some(SlotInfo { + label, + slot_type: SlotType { label: storage_type.label.clone(), dyn_sol_type: parsed_type }, + offset: 0, + slot: slot_str.to_string(), + members: None, + decoded: None, + }); } None From 1b1f62604611628f0beee474f767b61e55f1fce9 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:29:41 +0530 Subject: [PATCH 46/55] decode structs and members recursively --- crates/cheatcodes/src/evm.rs | 323 +++++++++++++++++------------------ 1 file changed, 153 insertions(+), 170 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 47a8d7faf10f7..0aa29411ac0d2 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -136,7 +136,9 @@ struct SlotInfo { /// Wrapper type that holds both the original type label and the DynSolType #[derive(Debug)] struct SlotType { - /// Original type label from storage layout (e.g., "struct DiffTest.TestStruct") + /// Parent type label from storage layout specifically used for structs. + /// + /// As members / fields of a struct have different labels. label: String, /// Parsed dynamic type for decoding dyn_sol_type: DynSolType, @@ -1649,78 +1651,15 @@ fn get_contract_data<'a>( artifacts.find_by_deployed_code(&code_bytes) } -/// Helper function to parse struct members from storage type. -fn parse_struct_members(storage_type: &StorageType) -> Option> { - storage_type - .other - .get("members") - .and_then(|v| serde_json::from_value::>(v.clone()).ok()) -} - -/// Helper function to check if a struct spans multiple slots. -fn is_multi_slot_struct(members: &[Storage]) -> bool { - !members.iter().all(|m| m.slot == "0") -} - -/// Helper function to parse a type label into DynSolType. -fn parse_type_label(label: &str) -> Option { - if label.starts_with("struct ") { - // For struct types, we return a Tuple as placeholder - // since we handle structs specially - Some(DynSolType::Tuple(vec![])) - } else { - DynSolType::parse(label).ok() - } -} - -/// Recursively finds the first non-struct member of a multi-slot struct. -/// Returns the label path and slot info for the first primitive member. -fn find_first_primitive_member( - storage_layout: &StorageLayout, - base_label: &str, - members: &[Storage], - slot_str: &str, -) -> Option { - let first_member = members.iter().find(|m| m.slot == "0")?; - let member_type_info = storage_layout.types.get(&first_member.storage_type)?; - let label_path = format!("{}.{}", base_label, first_member.label); - - // If the first member is itself a multi-slot struct, recurse - if member_type_info.label.starts_with("struct ") { - if let Some(inner_members) = parse_struct_members(member_type_info) { - if is_multi_slot_struct(&inner_members) { - return find_first_primitive_member( - storage_layout, - &label_path, - &inner_members, - slot_str, - ); - } - } - } - - // First member is a primitive or single-slot struct - let member_type = parse_type_label(&member_type_info.label)?; - Some(SlotInfo { - label: label_path, - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: first_member.offset, - slot: slot_str.to_string(), - decoded: None, - members: None, - }) -} - -/// Recursively searches for a member at the requested slot within nested structs. -fn find_member_at_slot( +/// Recursively processes a struct and finds the slot info for the requested slot. +/// This handles both accessing the struct itself and accessing its members at any depth. +fn handle_struct_recursive( storage_layout: &StorageLayout, base_label: &str, - members: &[Storage], + storage_type: &StorageType, base_slot: U256, requested_slot: U256, + offset: i64, slot_str: &str, depth: usize, ) -> Option { @@ -1729,32 +1668,131 @@ fn find_member_at_slot( if depth > MAX_DEPTH { return None; } - - for member in members { + + let members = storage_type + .other + .get("members") + .and_then(|v| serde_json::from_value::>(v.clone()).ok())?; + + // If this is the exact slot we're looking for (struct's base slot) + if base_slot == requested_slot { + // For structs, we need to determine what to return: + // - If all members are in the same slot (single-slot struct), return the struct with member + // info + // - If members span multiple slots, return the first member at this slot + + // Find the member at slot offset 0 (the member that starts at this slot) + if let Some(first_member) = members.iter().find(|m| m.slot == "0") { + let member_type_info = storage_layout.types.get(&first_member.storage_type)?; + + // Check if we have a single-slot struct (all members have slot "0") + let is_single_slot = members.iter().all(|m| m.slot == "0"); + + if is_single_slot { + // Build member info for single-slot struct + let mut member_infos = Vec::new(); + for member in &members { + if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + && let Some(member_type) = DynSolType::parse(&member_type_info.label).ok() + { + member_infos.push(SlotInfo { + label: member.label.clone(), + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str.to_string(), + members: None, + decoded: None, + }); + } + } + + // Build the CustomStruct type + let struct_name = + storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label); + let prop_names: Vec = members.iter().map(|m| m.label.clone()).collect(); + let member_types: Vec = + member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect(); + + let parsed_type = DynSolType::CustomStruct { + name: struct_name.to_string(), + prop_names, + tuple: member_types, + }; + + return Some(SlotInfo { + label: base_label.to_string(), + slot_type: SlotType { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset, + slot: slot_str.to_string(), + decoded: None, + members: if member_infos.is_empty() { None } else { Some(member_infos) }, + }); + } else { + // Multi-slot struct - return the first member. + let member_label = format!("{}.{}", base_label, first_member.label); + + // If the first member is itself a struct, recurse + if member_type_info.label.starts_with("struct ") { + return handle_struct_recursive( + storage_layout, + &member_label, + member_type_info, + base_slot, // First member is at the same slot + requested_slot, + first_member.offset, + slot_str, + depth + 1, + ); + } + + // Return the first member as a primitive + return Some(SlotInfo { + label: member_label, + slot_type: SlotType { + label: member_type_info.label.clone(), + dyn_sol_type: DynSolType::parse(&member_type_info.label).ok()?, + }, + offset: first_member.offset, + slot: slot_str.to_string(), + decoded: None, + members: None, + }); + } + } + } + + // Not the base slot - search through members + for member in &members { let member_slot_offset = U256::from_str(&member.slot).ok()?; let member_slot = base_slot + member_slot_offset; let member_type_info = storage_layout.types.get(&member.storage_type)?; let member_label = format!("{}.{}", base_label, member.label); - + if member_slot == requested_slot { - // Found the member at this slot - - // If it's a multi-slot struct, return its first primitive member + // Found the exact member slot + + // If this member is a struct, recurse into it if member_type_info.label.starts_with("struct ") { - if let Some(nested_members) = parse_struct_members(member_type_info) { - if is_multi_slot_struct(&nested_members) { - return find_first_primitive_member( - storage_layout, - &member_label, - &nested_members, - slot_str, - ); - } - } + return handle_struct_recursive( + storage_layout, + &member_label, + member_type_info, + member_slot, + requested_slot, + member.offset, + slot_str, + depth + 1, + ); } - - // Regular member or single-slot struct - let member_type = parse_type_label(&member_type_info.label)?; + + // Regular member + let member_type = DynSolType::parse(&member_type_info.label).ok()?; return Some(SlotInfo { label: member_label, slot_type: SlotType { @@ -1767,25 +1805,24 @@ fn find_member_at_slot( decoded: None, }); } - - // If this member is a struct, recursively search its members - if member_type_info.label.starts_with("struct ") { - if let Some(nested_members) = parse_struct_members(member_type_info) { - if let Some(slot_info) = find_member_at_slot( - storage_layout, - &member_label, - &nested_members, - member_slot, - requested_slot, - slot_str, - depth + 1, - ) { - return Some(slot_info); - } - } + + // If this member is a struct and the requested slot might be inside it, recurse + if member_type_info.label.starts_with("struct ") + && let Some(slot_info) = handle_struct_recursive( + storage_layout, + &member_label, + member_type_info, + member_slot, + requested_slot, + member.offset, + slot_str, + depth + 1, + ) + { + return Some(slot_info); } } - + None } @@ -1797,70 +1834,15 @@ fn handle_struct( requested_slot: U256, slot_str: &str, ) -> Option { - let members = parse_struct_members(storage_type)?; let base_slot = U256::from_str(&storage.slot).ok()?; - // Check if this is a direct slot match (accessing the struct's base slot) - if base_slot == requested_slot { - // Check if it's a multi-slot struct - if is_multi_slot_struct(&members) { - // For multi-slot structs at base slot, recursively find the first primitive member - return find_first_primitive_member(storage_layout, &storage.label, &members, slot_str); - } else { - // Single-slot struct - build member info - let mut member_infos = Vec::new(); - for member in &members { - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) - && let Some(member_type) = parse_type_label(&member_type_info.label) - { - member_infos.push(SlotInfo { - label: member.label.clone(), - slot_type: SlotType { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - } - - // Build the CustomStruct type - let struct_name = - storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label); - let prop_names: Vec = members.iter().map(|m| m.label.clone()).collect(); - let member_types: Vec = - member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect(); - - let parsed_type = DynSolType::CustomStruct { - name: struct_name.to_string(), - prop_names, - tuple: member_types, - }; - - return Some(SlotInfo { - label: storage.label.clone(), - slot_type: SlotType { - label: storage_type.label.clone(), - dyn_sol_type: parsed_type, - }, - offset: storage.offset, - slot: slot_str.to_string(), - decoded: None, - members: if member_infos.is_empty() { None } else { Some(member_infos) }, - }); - } - } - - // Not a direct match - recursively search for the member at the requested slot - find_member_at_slot( + handle_struct_recursive( storage_layout, &storage.label, - &members, + storage_type, base_slot, requested_slot, + storage.offset, slot_str, 0, // Start with depth 0 ) @@ -1909,17 +1891,18 @@ fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option Date: Wed, 20 Aug 2025 19:17:25 +0530 Subject: [PATCH 47/55] cleanup --- crates/cheatcodes/src/evm.rs | 344 ++++++++++++++++++----------------- testdata/foundry.toml | 2 +- 2 files changed, 177 insertions(+), 169 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 0aa29411ac0d2..eb95ff193eb45 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -120,9 +120,15 @@ struct SlotStateDiff { #[derive(Serialize, Debug)] struct SlotInfo { + /// The variable name from the storage layout. + /// + /// For top-level variables: just the variable name (e.g., "myVariable") + /// For struct members: dotted path (e.g., "myStruct.memberName") + /// For array elements: name with indices (e.g., "myArray[0]", "matrix[1][2]") + /// For nested structures: full path (e.g., "outer.inner.field") label: String, #[serde(rename = "type", serialize_with = "serialize_slot_type")] - slot_type: SlotType, + slot_type: StorageTypeInfo, offset: i64, slot: String, /// For struct members, contains nested SlotInfo for each member @@ -133,18 +139,21 @@ struct SlotInfo { decoded: Option, } -/// Wrapper type that holds both the original type label and the DynSolType +/// Wrapper type that holds both the original type label and the parsed DynSolType. +/// +/// We need both because: +/// - `label`: Contains the exact Solidity type string from the storage layout (e.g., "struct +/// Contract.StructName", "uint256", "address[2][3]") +/// - `dyn_sol_type`: The parsed type used for actual value decoding #[derive(Debug)] -struct SlotType { - /// Parent type label from storage layout specifically used for structs. - /// - /// As members / fields of a struct have different labels. +struct StorageTypeInfo { + /// This label is used during serialization to ensure the output matches + /// what users expect to see in the state diff JSON. label: String, - /// Parsed dynamic type for decoding dyn_sol_type: DynSolType, } -fn serialize_slot_type(slot_type: &SlotType, serializer: S) -> Result +fn serialize_slot_type(slot_type: &StorageTypeInfo, serializer: S) -> Result where S: serde::Serializer, { @@ -1651,16 +1660,145 @@ fn get_contract_data<'a>( artifacts.find_by_deployed_code(&code_bytes) } +/// Gets storage layout info for a specific slot. +fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { + let slot = U256::from_be_bytes(slot.0); + let slot_str = slot.to_string(); + + for storage in &storage_layout.storage { + trace!( + "Checking storage item: label={}, slot={}, type={}", + storage.label, storage.slot, storage.storage_type + ); + + let storage_type = storage_layout.types.get(&storage.storage_type)?; + let dyn_type = DynSolType::parse(&storage_type.label).ok(); + + // Check if we're able to match on a slot from the layout + if storage.slot == slot_str + && let Some(parsed_type) = dyn_type + { + // Successfully parsed - handle arrays or simple types + let label = if let DynSolType::FixedArray(_, _) = &parsed_type { + format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) + } else { + storage.label.clone() + }; + + return Some(SlotInfo { + label, + slot_type: StorageTypeInfo { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset: storage.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + }); + } + + // Handle the case where the accessed `slot` if maybe different from the base slot. + let array_start_slot = U256::from_str(&storage.slot).ok()?; + + if let Some(parsed_type) = dyn_type + && let DynSolType::FixedArray(_, _) = parsed_type + && let Some(slot_info) = + handle_array_slot(storage, storage_type, slot, array_start_slot, &slot_str) + { + return Some(slot_info); + } + + // If type parsing fails and the label is a struct + if storage_type.label.starts_with("struct ") + && let Some(slot_info) = + handle_struct(storage, storage_type, storage_layout, slot, &slot_str) + { + return Some(slot_info); + } + } + + None +} + +/// Handles array slot access. +fn handle_array_slot( + storage: &Storage, + storage_type: &StorageType, + slot: U256, + array_start_slot: U256, // The slot where this array begins + slot_str: &str, +) -> Option { + // Check if slot is within array bounds + let total_bytes = storage_type.number_of_bytes.parse::().ok()?; + let total_slots = total_bytes.div_ceil(32); + + if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) { + let parsed_type = DynSolType::parse(&storage_type.label).ok()?; + let index = (slot - array_start_slot).to::(); + // Format the array element label based on array dimensions + let label = match &parsed_type { + 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); + let col = index % (*inner_size as u64); + format!("{}[{row}][{col}]", storage.label) + } else { + // 1D array + format!("{}[{index}]", storage.label) + } + } + _ => storage.label.clone(), + }; + + return Some(SlotInfo { + label, + slot_type: StorageTypeInfo { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset: 0, + slot: slot_str.to_string(), + members: None, + decoded: None, + }); + } + + None +} + +/// Returns the base index [\0\] or [\0\][\0\] for a fixed array type depending on the dimensions. +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 { + // Simple 1D array + "[0]".to_string() + } + } + _ => String::new(), + } +} + +/// Context for recursive struct processing +struct StructContext<'a> { + storage_layout: &'a StorageLayout, + target_slot: U256, // The slot we're trying to decode + slot_str: String, // String representation of target_slot +} + /// Recursively processes a struct and finds the slot info for the requested slot. /// This handles both accessing the struct itself and accessing its members at any depth. fn handle_struct_recursive( - storage_layout: &StorageLayout, + ctx: &StructContext, base_label: &str, storage_type: &StorageType, - base_slot: U256, - requested_slot: U256, + struct_start_slot: U256, // The slot where this struct begins offset: i64, - slot_str: &str, depth: usize, ) -> Option { // Limit recursion depth to prevent stack overflow @@ -1675,7 +1813,7 @@ fn handle_struct_recursive( .and_then(|v| serde_json::from_value::>(v.clone()).ok())?; // If this is the exact slot we're looking for (struct's base slot) - if base_slot == requested_slot { + if struct_start_slot == ctx.target_slot { // For structs, we need to determine what to return: // - If all members are in the same slot (single-slot struct), return the struct with member // info @@ -1683,7 +1821,7 @@ fn handle_struct_recursive( // Find the member at slot offset 0 (the member that starts at this slot) if let Some(first_member) = members.iter().find(|m| m.slot == "0") { - let member_type_info = storage_layout.types.get(&first_member.storage_type)?; + let member_type_info = ctx.storage_layout.types.get(&first_member.storage_type)?; // Check if we have a single-slot struct (all members have slot "0") let is_single_slot = members.iter().all(|m| m.slot == "0"); @@ -1692,17 +1830,18 @@ fn handle_struct_recursive( // Build member info for single-slot struct let mut member_infos = Vec::new(); for member in &members { - if let Some(member_type_info) = storage_layout.types.get(&member.storage_type) + if let Some(member_type_info) = + ctx.storage_layout.types.get(&member.storage_type) && let Some(member_type) = DynSolType::parse(&member_type_info.label).ok() { member_infos.push(SlotInfo { label: member.label.clone(), - slot_type: SlotType { + slot_type: StorageTypeInfo { label: member_type_info.label.clone(), dyn_sol_type: member_type, }, offset: member.offset, - slot: slot_str.to_string(), + slot: ctx.slot_str.clone(), members: None, decoded: None, }); @@ -1724,12 +1863,12 @@ fn handle_struct_recursive( return Some(SlotInfo { label: base_label.to_string(), - slot_type: SlotType { + slot_type: StorageTypeInfo { label: storage_type.label.clone(), dyn_sol_type: parsed_type, }, offset, - slot: slot_str.to_string(), + slot: ctx.slot_str.clone(), decoded: None, members: if member_infos.is_empty() { None } else { Some(member_infos) }, }); @@ -1740,13 +1879,11 @@ fn handle_struct_recursive( // If the first member is itself a struct, recurse if member_type_info.label.starts_with("struct ") { return handle_struct_recursive( - storage_layout, + ctx, &member_label, member_type_info, - base_slot, // First member is at the same slot - requested_slot, + struct_start_slot, // First member is at the same slot first_member.offset, - slot_str, depth + 1, ); } @@ -1754,12 +1891,12 @@ fn handle_struct_recursive( // Return the first member as a primitive return Some(SlotInfo { label: member_label, - slot_type: SlotType { + slot_type: StorageTypeInfo { label: member_type_info.label.clone(), dyn_sol_type: DynSolType::parse(&member_type_info.label).ok()?, }, offset: first_member.offset, - slot: slot_str.to_string(), + slot: ctx.slot_str.clone(), decoded: None, members: None, }); @@ -1770,23 +1907,21 @@ fn handle_struct_recursive( // Not the base slot - search through members for member in &members { let member_slot_offset = U256::from_str(&member.slot).ok()?; - let member_slot = base_slot + member_slot_offset; - let member_type_info = storage_layout.types.get(&member.storage_type)?; + let member_slot = struct_start_slot + member_slot_offset; + let member_type_info = ctx.storage_layout.types.get(&member.storage_type)?; let member_label = format!("{}.{}", base_label, member.label); - if member_slot == requested_slot { + if member_slot == ctx.target_slot { // Found the exact member slot // If this member is a struct, recurse into it if member_type_info.label.starts_with("struct ") { return handle_struct_recursive( - storage_layout, + ctx, &member_label, member_type_info, member_slot, - requested_slot, member.offset, - slot_str, depth + 1, ); } @@ -1795,12 +1930,12 @@ fn handle_struct_recursive( let member_type = DynSolType::parse(&member_type_info.label).ok()?; return Some(SlotInfo { label: member_label, - slot_type: SlotType { + slot_type: StorageTypeInfo { label: member_type_info.label.clone(), dyn_sol_type: member_type, }, offset: member.offset, - slot: slot_str.to_string(), + slot: ctx.slot_str.clone(), members: None, decoded: None, }); @@ -1809,13 +1944,11 @@ fn handle_struct_recursive( // If this member is a struct and the requested slot might be inside it, recurse if member_type_info.label.starts_with("struct ") && let Some(slot_info) = handle_struct_recursive( - storage_layout, + ctx, &member_label, member_type_info, member_slot, - requested_slot, member.offset, - slot_str, depth + 1, ) { @@ -1831,148 +1964,23 @@ fn handle_struct( storage: &Storage, storage_type: &StorageType, storage_layout: &StorageLayout, - requested_slot: U256, + target_slot: U256, slot_str: &str, ) -> Option { - let base_slot = U256::from_str(&storage.slot).ok()?; + let struct_start_slot = U256::from_str(&storage.slot).ok()?; + + let ctx = StructContext { storage_layout, target_slot, slot_str: slot_str.to_string() }; handle_struct_recursive( - storage_layout, + &ctx, &storage.label, storage_type, - base_slot, - requested_slot, + struct_start_slot, storage.offset, - slot_str, - 0, // Start with depth 0 + 0, ) } -/// Gets storage layout info for a specific slot. -fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { - let slot = U256::from_be_bytes(slot.0); - let slot_str = slot.to_string(); - - for storage in &storage_layout.storage { - trace!( - "Checking storage item: label={}, slot={}, type={}", - storage.label, storage.slot, storage.storage_type - ); - - let storage_type = storage_layout.types.get(&storage.storage_type)?; - let dyn_type = DynSolType::parse(&storage_type.label).ok(); - - // Check if we're able to match on a slot from the layout - if storage.slot == slot_str - && let Some(parsed_type) = dyn_type - { - // Successfully parsed - handle arrays or simple types - let label = if let DynSolType::FixedArray(_, _) = &parsed_type { - format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) - } else { - storage.label.clone() - }; - - return Some(SlotInfo { - label, - slot_type: SlotType { - label: storage_type.label.clone(), - dyn_sol_type: parsed_type, - }, - offset: storage.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - - // Handle the case where the accessed `slot` if maybe different from the base slot. - let base_slot = U256::from_str(&storage.slot).ok()?; - - if let Some(parsed_type) = dyn_type - && let DynSolType::FixedArray(_, _) = parsed_type - && let Some(slot_info) = - handle_array_slot(storage, storage_type, slot, base_slot, &slot_str) - { - return Some(slot_info); - } - - // If type parsing fails and the label is a struct - if storage_type.label.starts_with("struct ") - && let Some(slot_info) = - handle_struct(storage, storage_type, storage_layout, slot, &slot_str) - { - return Some(slot_info); - } - } - - None -} - -/// Handles array slot access. -fn handle_array_slot( - storage: &Storage, - storage_type: &StorageType, - slot: U256, - base_slot: U256, - slot_str: &str, -) -> Option { - // Check if slot is within array bounds - let total_bytes = storage_type.number_of_bytes.parse::().ok()?; - let total_slots = total_bytes.div_ceil(32); - - if slot >= base_slot && slot < base_slot + U256::from(total_slots) { - let parsed_type = DynSolType::parse(&storage_type.label).ok()?; - let index = (slot - base_slot).to::(); - let label = format_array_element_label(&storage.label, &parsed_type, index); - - return Some(SlotInfo { - label, - slot_type: SlotType { label: storage_type.label.clone(), dyn_sol_type: parsed_type }, - offset: 0, - slot: slot_str.to_string(), - members: None, - decoded: None, - }); - } - - None -} - -/// Returns the base index [\0\] or [\0\][\0\] for a fixed array type depending on the dimensions. -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 { - // Simple 1D array - "[0]".to_string() - } - } - _ => String::new(), - } -} - -/// 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, _) => { - 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 decode a single storage value using its DynSolType fn decode_storage_value( value: B256, diff --git a/testdata/foundry.toml b/testdata/foundry.toml index ea23af5935fb0..e9189bb008a32 100644 --- a/testdata/foundry.toml +++ b/testdata/foundry.toml @@ -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 a1de1dc4d6773a707421efdda105d4bfec7a1ace Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:21:27 +0530 Subject: [PATCH 48/55] doc fix --- crates/cheatcodes/src/evm.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index eb95ff193eb45..cf98fc942b8c9 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -124,7 +124,7 @@ struct SlotInfo { /// /// For top-level variables: just the variable name (e.g., "myVariable") /// For struct members: dotted path (e.g., "myStruct.memberName") - /// For array elements: name with indices (e.g., "myArray[0]", "matrix[1][2]") + /// For array elements: name with indices (e.g., "myArray\[0\]", "matrix\[1\]\[2\]") /// For nested structures: full path (e.g., "outer.inner.field") label: String, #[serde(rename = "type", serialize_with = "serialize_slot_type")] @@ -143,7 +143,7 @@ struct SlotInfo { /// /// We need both because: /// - `label`: Contains the exact Solidity type string from the storage layout (e.g., "struct -/// Contract.StructName", "uint256", "address[2][3]") +/// Contract.StructName", "uint256", "address\[2\]\[3\]") /// - `dyn_sol_type`: The parsed type used for actual value decoding #[derive(Debug)] struct StorageTypeInfo { From c9d1cefde1ccf060611dae91030c2f9a1d094674 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:30:46 +0530 Subject: [PATCH 49/55] feat(cheatcodes): decode mappings in state diffs (#11381) * feat(cheatcodes): decode mappings in state diffs * feat: decode nested mappings * assert vm.getStateDiff output * feat: add `keys` fields to `SlotInfo` in case of mappings * remove wrapper * refactor: moves state diff decoding to common (#11413) * refactor: storage decoder * cleanup * dedup MappingSlots by moving it to common * move decoding logic into SlotInfo * rename to SlotIndentifier * docs * fix: delegate identification according to encoding types * clippy + fmt * docs fix * fix * merge match arms * merge ifs * recurse handle_struct --- crates/cheatcodes/src/evm.rs | 704 ++---------------- crates/cheatcodes/src/evm/mapping.rs | 44 +- crates/cheatcodes/src/inspector.rs | 7 +- crates/common/src/lib.rs | 2 + crates/common/src/mapping_slots.rs | 39 + crates/common/src/slot_identifier.rs | 693 +++++++++++++++++ .../default/cheats/StateDiffMappings.t.sol | 315 ++++++++ .../default/cheats/StateDiffStructTest.t.sol | 10 +- 8 files changed, 1120 insertions(+), 694 deletions(-) create mode 100644 crates/common/src/mapping_slots.rs create mode 100644 crates/common/src/slot_identifier.rs create mode 100644 testdata/default/cheats/StateDiffMappings.t.sol diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index cf98fc942b8c9..37125dabb73bd 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -6,14 +6,15 @@ use crate::{ inspector::{Ecx, RecordDebugStepInfo}, }; use alloy_consensus::TxEnvelope; -use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_genesis::{Genesis, GenesisAccount}; use alloy_network::eip2718::EIP4844_TX_TYPE_ID; -use alloy_primitives::{Address, B256, U256, hex, map::HashMap}; +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, StorageType}; +use foundry_common::{ + fs::{read_json_file, write_json_file}, + slot_identifier::{SlotIdentifier, SlotInfo, format_value}, +}; use foundry_evm_core::{ ContextExt, backend::{DatabaseExt, RevertStateSnapshotAction}, @@ -33,7 +34,6 @@ use std::{ collections::{BTreeMap, HashSet, btree_map::Entry}, fmt::Display, path::Path, - str::FromStr, }; mod record_debug_step; @@ -107,92 +107,13 @@ struct SlotStateDiff { previous_value: B256, /// Current storage value. new_value: B256, - /// 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, - /// Storage layout metadata (variable name, type, offset). /// Only present when contract has storage layout output. + /// This includes decoded values when available. #[serde(skip_serializing_if = "Option::is_none", flatten)] slot_info: Option, } -#[derive(Serialize, Debug)] -struct SlotInfo { - /// The variable name from the storage layout. - /// - /// For top-level variables: just the variable name (e.g., "myVariable") - /// For struct members: dotted path (e.g., "myStruct.memberName") - /// For array elements: name with indices (e.g., "myArray\[0\]", "matrix\[1\]\[2\]") - /// For nested structures: full path (e.g., "outer.inner.field") - label: String, - #[serde(rename = "type", serialize_with = "serialize_slot_type")] - slot_type: StorageTypeInfo, - offset: i64, - slot: String, - /// For struct members, contains nested SlotInfo for each member - #[serde(skip_serializing_if = "Option::is_none")] - members: Option>, - /// Decoded values (if available) - used for struct members - #[serde(skip_serializing_if = "Option::is_none")] - decoded: Option, -} - -/// Wrapper type that holds both the original type label and the parsed DynSolType. -/// -/// We need both because: -/// - `label`: Contains the exact Solidity type string from the storage layout (e.g., "struct -/// Contract.StructName", "uint256", "address\[2\]\[3\]") -/// - `dyn_sol_type`: The parsed type used for actual value decoding -#[derive(Debug)] -struct StorageTypeInfo { - /// This label is used during serialization to ensure the output matches - /// what users expect to see in the state diff JSON. - label: String, - dyn_sol_type: DynSolType, -} - -fn serialize_slot_type(slot_type: &StorageTypeInfo, serializer: S) -> Result -where - S: serde::Serializer, -{ - // For CustomStruct, format as "struct Name", otherwise use the label - let type_str = match &slot_type.dyn_sol_type { - DynSolType::CustomStruct { name, .. } => { - // If the label already has "struct " prefix, use it as-is - if slot_type.label.starts_with("struct ") { - slot_type.label.clone() - } else { - format!("struct {name}") - } - } - _ => slot_type.label.clone(), - }; - serializer.serialize_str(&type_str) -} - -#[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")] @@ -258,30 +179,32 @@ impl Display for AccountStateDiffs { if !&self.state_diff.is_empty() { writeln!(f, "- state diff:")?; 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 - only show decoded values - writeln!( - f, - "@ {slot} ({}, {}): {} → {}", - slot_info.label, - slot_info.slot_type.dyn_sol_type, - 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 - show raw hex values - writeln!( - f, - "@ {slot} ({}, {}): {} → {}", - slot_info.label, - slot_info.slot_type.dyn_sol_type, - slot_changes.previous_value, - slot_changes.new_value - )?; + match &slot_changes.slot_info { + Some(slot_info) => { + if slot_info.decoded.is_some() { + // Have slot info with decoded values - show decoded values + let decoded = slot_info.decoded.as_ref().unwrap(); + writeln!( + f, + "@ {slot} ({}, {}): {} → {}", + slot_info.label, + slot_info.slot_type.dyn_sol_type, + format_value(&decoded.previous_value), + format_value(&decoded.new_value) + )?; + } else { + // Have slot info but no decoded values - show raw hex values + writeln!( + f, + "@ {slot} ({}, {}): {} → {}", + slot_info.label, + slot_info.slot_type.dyn_sol_type, + slot_changes.previous_value, + slot_changes.new_value + )?; + } } - _ => { + None => { // No slot info - show raw hex values writeln!( f, @@ -942,6 +865,10 @@ impl Cheatcode for startStateDiffRecordingCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self {} = self; state.recorded_account_diffs_stack = Some(Default::default()); + // Enable mapping recording to track mapping slot accesses + if state.mapping_slots.is_none() { + state.mapping_slots = Some(Default::default()); + } Ok(Default::default()) } } @@ -1458,143 +1385,42 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap { // Get storage layout info for this slot - let slot_info = layout - .and_then(|layout| get_slot_info(layout, &storage_access.slot)); - - // Try to decode values if we have slot info - let (decoded, slot_info_with_decoded) = if let Some(mut info) = - slot_info - { - // Check if this is a struct with members - if let Some(ref mut members) = info.members { - // Decode each member individually - for member in members.iter_mut() { - let offset = member.offset as usize; - let size = match &member.slot_type.dyn_sol_type { - DynSolType::Uint(bits) | DynSolType::Int(bits) => { - bits / 8 - } - DynSolType::Address => 20, - DynSolType::Bool => 1, - DynSolType::FixedBytes(size) => *size, - _ => 32, // Default to full word - }; - - // Extract and decode member values - let mut prev_bytes = [0u8; 32]; - let mut new_bytes = [0u8; 32]; - - if offset + size <= 32 { - // In Solidity storage, values are right-aligned - // For offset 0, we want the rightmost bytes - // For offset 16 (for a uint128), we want bytes - // 0-16 - // For packed storage: offset 0 is at the rightmost - // position - // offset 0, size 16 -> read bytes 16-32 (rightmost) - // offset 16, size 16 -> read bytes 0-16 (leftmost) - let byte_start = 32 - offset - size; - prev_bytes[32 - size..].copy_from_slice( - &storage_access.previousValue.0 - [byte_start..byte_start + size], - ); - new_bytes[32 - size..].copy_from_slice( - &storage_access.newValue.0 - [byte_start..byte_start + size], - ); - } - - // Decode the member values - if let (Ok(prev_val), Ok(new_val)) = ( - member - .slot_type - .dyn_sol_type - .abi_decode(&prev_bytes), - member - .slot_type - .dyn_sol_type - .abi_decode(&new_bytes), - ) { - member.decoded = Some(DecodedSlotValues { - previous_value: prev_val, - new_value: new_val, - }); - } - } - // For structs with members, we don't need a top-level - // decoded value - (None, Some(info)) - } else { - // Not a struct, decode as a single value - let storage_layout = - storage_layouts.get(&storage_access.account); - let storage_type = storage_layout.and_then(|layout| { - layout - .storage - .iter() - .find(|s| s.slot == info.slot) - .and_then(|s| layout.types.get(&s.storage_type)) - }); - - let decoded = if let (Some(prev), Some(new)) = ( - decode_storage_value( - storage_access.previousValue, - &info.slot_type.dyn_sol_type, - storage_type, - storage_layout.as_ref().map(|arc| arc.as_ref()), - ), - decode_storage_value( - storage_access.newValue, - &info.slot_type.dyn_sol_type, - storage_type, - storage_layout.as_ref().map(|arc| arc.as_ref()), - ), - ) { - Some(DecodedSlotValues { - previous_value: prev, - new_value: new, - }) - } else { - None - }; - (decoded, Some(info)) - } - } else { - (None, None) - }; + // Include mapping slots if available for the account + let mapping_slots = ccx + .state + .mapping_slots + .as_ref() + .and_then(|slots| slots.get(&storage_access.account)); + + let mut slot_info = layout.and_then(|layout| { + let decoder = SlotIdentifier::new(layout.clone()); + decoder.identify(&storage_access.slot, mapping_slots) + }); + + // Decode values if we have slot info + if let Some(ref mut info) = slot_info { + info.decode_values( + storage_access.previousValue, + storage_access.newValue, + ); + } slot_state_diff.insert(SlotStateDiff { previous_value: storage_access.previousValue, new_value: storage_access.newValue, - decoded, - slot_info: slot_info_with_decoded, + slot_info, }); } Entry::Occupied(mut slot_state_diff) => { 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 - { - // Get storage type info - let storage_type = layout.and_then(|layout| { - // Find the storage item that matches this slot - layout - .storage - .iter() - .find(|s| s.slot == slot_info.slot) - .and_then(|s| layout.types.get(&s.storage_type)) - }); - - if let Some(new_value) = decode_storage_value( + // Update decoded values if we have slot info + if let Some(ref mut slot_info) = entry.slot_info { + slot_info.decode_values( + entry.previous_value, storage_access.newValue, - &slot_info.slot_type.dyn_sol_type, - storage_type, - layout.as_ref().map(|arc| arc.as_ref()), - ) { - decoded.new_value = new_value; - } + ); } } } @@ -1660,416 +1486,6 @@ fn get_contract_data<'a>( artifacts.find_by_deployed_code(&code_bytes) } -/// Gets storage layout info for a specific slot. -fn get_slot_info(storage_layout: &StorageLayout, slot: &B256) -> Option { - let slot = U256::from_be_bytes(slot.0); - let slot_str = slot.to_string(); - - for storage in &storage_layout.storage { - trace!( - "Checking storage item: label={}, slot={}, type={}", - storage.label, storage.slot, storage.storage_type - ); - - let storage_type = storage_layout.types.get(&storage.storage_type)?; - let dyn_type = DynSolType::parse(&storage_type.label).ok(); - - // Check if we're able to match on a slot from the layout - if storage.slot == slot_str - && let Some(parsed_type) = dyn_type - { - // Successfully parsed - handle arrays or simple types - let label = if let DynSolType::FixedArray(_, _) = &parsed_type { - format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) - } else { - storage.label.clone() - }; - - return Some(SlotInfo { - label, - slot_type: StorageTypeInfo { - label: storage_type.label.clone(), - dyn_sol_type: parsed_type, - }, - offset: storage.offset, - slot: storage.slot.clone(), - members: None, - decoded: None, - }); - } - - // Handle the case where the accessed `slot` if maybe different from the base slot. - let array_start_slot = U256::from_str(&storage.slot).ok()?; - - if let Some(parsed_type) = dyn_type - && let DynSolType::FixedArray(_, _) = parsed_type - && let Some(slot_info) = - handle_array_slot(storage, storage_type, slot, array_start_slot, &slot_str) - { - return Some(slot_info); - } - - // If type parsing fails and the label is a struct - if storage_type.label.starts_with("struct ") - && let Some(slot_info) = - handle_struct(storage, storage_type, storage_layout, slot, &slot_str) - { - return Some(slot_info); - } - } - - None -} - -/// Handles array slot access. -fn handle_array_slot( - storage: &Storage, - storage_type: &StorageType, - slot: U256, - array_start_slot: U256, // The slot where this array begins - slot_str: &str, -) -> Option { - // Check if slot is within array bounds - let total_bytes = storage_type.number_of_bytes.parse::().ok()?; - let total_slots = total_bytes.div_ceil(32); - - if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) { - let parsed_type = DynSolType::parse(&storage_type.label).ok()?; - let index = (slot - array_start_slot).to::(); - // Format the array element label based on array dimensions - let label = match &parsed_type { - 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); - let col = index % (*inner_size as u64); - format!("{}[{row}][{col}]", storage.label) - } else { - // 1D array - format!("{}[{index}]", storage.label) - } - } - _ => storage.label.clone(), - }; - - return Some(SlotInfo { - label, - slot_type: StorageTypeInfo { - label: storage_type.label.clone(), - dyn_sol_type: parsed_type, - }, - offset: 0, - slot: slot_str.to_string(), - members: None, - decoded: None, - }); - } - - None -} - -/// Returns the base index [\0\] or [\0\][\0\] for a fixed array type depending on the dimensions. -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 { - // Simple 1D array - "[0]".to_string() - } - } - _ => String::new(), - } -} - -/// Context for recursive struct processing -struct StructContext<'a> { - storage_layout: &'a StorageLayout, - target_slot: U256, // The slot we're trying to decode - slot_str: String, // String representation of target_slot -} - -/// Recursively processes a struct and finds the slot info for the requested slot. -/// This handles both accessing the struct itself and accessing its members at any depth. -fn handle_struct_recursive( - ctx: &StructContext, - base_label: &str, - storage_type: &StorageType, - struct_start_slot: U256, // The slot where this struct begins - offset: i64, - depth: usize, -) -> Option { - // Limit recursion depth to prevent stack overflow - const MAX_DEPTH: usize = 10; - if depth > MAX_DEPTH { - return None; - } - - let members = storage_type - .other - .get("members") - .and_then(|v| serde_json::from_value::>(v.clone()).ok())?; - - // If this is the exact slot we're looking for (struct's base slot) - if struct_start_slot == ctx.target_slot { - // For structs, we need to determine what to return: - // - If all members are in the same slot (single-slot struct), return the struct with member - // info - // - If members span multiple slots, return the first member at this slot - - // Find the member at slot offset 0 (the member that starts at this slot) - if let Some(first_member) = members.iter().find(|m| m.slot == "0") { - let member_type_info = ctx.storage_layout.types.get(&first_member.storage_type)?; - - // Check if we have a single-slot struct (all members have slot "0") - let is_single_slot = members.iter().all(|m| m.slot == "0"); - - if is_single_slot { - // Build member info for single-slot struct - let mut member_infos = Vec::new(); - for member in &members { - if let Some(member_type_info) = - ctx.storage_layout.types.get(&member.storage_type) - && let Some(member_type) = DynSolType::parse(&member_type_info.label).ok() - { - member_infos.push(SlotInfo { - label: member.label.clone(), - slot_type: StorageTypeInfo { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: ctx.slot_str.clone(), - members: None, - decoded: None, - }); - } - } - - // Build the CustomStruct type - let struct_name = - storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label); - let prop_names: Vec = members.iter().map(|m| m.label.clone()).collect(); - let member_types: Vec = - member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect(); - - let parsed_type = DynSolType::CustomStruct { - name: struct_name.to_string(), - prop_names, - tuple: member_types, - }; - - return Some(SlotInfo { - label: base_label.to_string(), - slot_type: StorageTypeInfo { - label: storage_type.label.clone(), - dyn_sol_type: parsed_type, - }, - offset, - slot: ctx.slot_str.clone(), - decoded: None, - members: if member_infos.is_empty() { None } else { Some(member_infos) }, - }); - } else { - // Multi-slot struct - return the first member. - let member_label = format!("{}.{}", base_label, first_member.label); - - // If the first member is itself a struct, recurse - if member_type_info.label.starts_with("struct ") { - return handle_struct_recursive( - ctx, - &member_label, - member_type_info, - struct_start_slot, // First member is at the same slot - first_member.offset, - depth + 1, - ); - } - - // Return the first member as a primitive - return Some(SlotInfo { - label: member_label, - slot_type: StorageTypeInfo { - label: member_type_info.label.clone(), - dyn_sol_type: DynSolType::parse(&member_type_info.label).ok()?, - }, - offset: first_member.offset, - slot: ctx.slot_str.clone(), - decoded: None, - members: None, - }); - } - } - } - - // Not the base slot - search through members - for member in &members { - let member_slot_offset = U256::from_str(&member.slot).ok()?; - let member_slot = struct_start_slot + member_slot_offset; - let member_type_info = ctx.storage_layout.types.get(&member.storage_type)?; - let member_label = format!("{}.{}", base_label, member.label); - - if member_slot == ctx.target_slot { - // Found the exact member slot - - // If this member is a struct, recurse into it - if member_type_info.label.starts_with("struct ") { - return handle_struct_recursive( - ctx, - &member_label, - member_type_info, - member_slot, - member.offset, - depth + 1, - ); - } - - // Regular member - let member_type = DynSolType::parse(&member_type_info.label).ok()?; - return Some(SlotInfo { - label: member_label, - slot_type: StorageTypeInfo { - label: member_type_info.label.clone(), - dyn_sol_type: member_type, - }, - offset: member.offset, - slot: ctx.slot_str.clone(), - members: None, - decoded: None, - }); - } - - // If this member is a struct and the requested slot might be inside it, recurse - if member_type_info.label.starts_with("struct ") - && let Some(slot_info) = handle_struct_recursive( - ctx, - &member_label, - member_type_info, - member_slot, - member.offset, - depth + 1, - ) - { - return Some(slot_info); - } - } - - None -} - -/// Handles struct slot decoding for both direct and member slot access. -fn handle_struct( - storage: &Storage, - storage_type: &StorageType, - storage_layout: &StorageLayout, - target_slot: U256, - slot_str: &str, -) -> Option { - let struct_start_slot = U256::from_str(&storage.slot).ok()?; - - let ctx = StructContext { storage_layout, target_slot, slot_str: slot_str.to_string() }; - - handle_struct_recursive( - &ctx, - &storage.label, - storage_type, - struct_start_slot, - storage.offset, - 0, - ) -} - -/// Helper function to decode a single storage value using its DynSolType -fn decode_storage_value( - value: B256, - dyn_type: &DynSolType, - storage_type: Option<&StorageType>, - storage_layout: Option<&StorageLayout>, -) -> 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(); - } - - // For tuples (structs), we need to decode each member based on its offset - if let DynSolType::Tuple(member_types) = actual_type { - // If we have storage type info with members, decode each member from the value - if let Some(st) = storage_type - && let Some(members_value) = st.other.get("members") - && let Ok(members) = serde_json::from_value::>(members_value.clone()) - && members.len() == member_types.len() - { - let mut decoded_members = Vec::new(); - - for (i, member) in members.iter().enumerate() { - // Get the member type - let member_type = &member_types[i]; - - // Calculate byte range for this member - let offset = member.offset as usize; - let member_storage_type = - storage_layout.and_then(|sl| sl.types.get(&member.storage_type)); - let size = member_storage_type - .and_then(|t| t.number_of_bytes.parse::().ok()) - .unwrap_or(32); - - // Extract bytes for this member from the full value - let mut member_bytes = [0u8; 32]; - if offset + size <= 32 { - // For packed storage: offset 0 is at the rightmost position - // offset 0, size 16 -> read bytes 16-32 (rightmost) - // offset 16, size 16 -> read bytes 0-16 (leftmost) - let byte_start = 32 - offset - size; - member_bytes[32 - size..] - .copy_from_slice(&value.0[byte_start..byte_start + size]); - } - - // Decode the member value - if let Ok(decoded) = member_type.abi_decode(&member_bytes) { - decoded_members.push(decoded); - } else { - return None; - } - } - - return Some(DynSolValue::Tuple(decoded_members)); - } - } - - // Use abi_decode to decode the value for non-struct types - 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/crates/cheatcodes/src/evm/mapping.rs b/crates/cheatcodes/src/evm/mapping.rs index 1c4dfe6086129..fd8534afdb628 100644 --- a/crates/cheatcodes/src/evm/mapping.rs +++ b/crates/cheatcodes/src/evm/mapping.rs @@ -1,52 +1,12 @@ use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; -use alloy_primitives::{ - Address, B256, U256, keccak256, - map::{AddressHashMap, B256HashMap}, -}; +use alloy_primitives::{Address, B256, U256, keccak256, map::AddressHashMap}; use alloy_sol_types::SolValue; +use foundry_common::mapping_slots::MappingSlots; use revm::{ bytecode::opcode, interpreter::{Interpreter, interpreter_types::Jumps}, }; -/// Recorded mapping slots. -#[derive(Clone, Debug, Default)] -pub struct MappingSlots { - /// Holds mapping parent (slots => slots) - pub parent_slots: B256HashMap, - - /// Holds mapping key (slots => key) - pub keys: B256HashMap, - - /// Holds mapping child (slots => slots[]) - pub children: B256HashMap>, - - /// Holds the last sha3 result `sha3_result => (data_low, data_high)`, this would only record - /// when sha3 is called with `size == 0x40`, and the lower 256 bits would be stored in - /// `data_low`, higher 256 bits in `data_high`. - /// This is needed for mapping_key detect if the slot is for some mapping and record that. - pub seen_sha3: B256HashMap<(B256, B256)>, -} - -impl MappingSlots { - /// Tries to insert a mapping slot. Returns true if it was inserted. - pub fn insert(&mut self, slot: B256) -> bool { - match self.seen_sha3.get(&slot).copied() { - Some((key, parent)) => { - if self.keys.contains_key(&slot) { - return false; - } - self.keys.insert(slot, key); - self.parent_slots.insert(slot, parent); - self.children.entry(parent).or_default().push(slot); - self.insert(parent); - true - } - None => false, - } - } -} - impl Cheatcode for startMappingRecordingCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self {} = self; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 63f0a60cb9a0c..db25de259ca1b 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -4,8 +4,7 @@ use crate::{ CheatsConfig, CheatsCtxt, DynCheatcode, Error, Result, Vm::{self, AccountAccess}, evm::{ - DealRecord, GasRecord, RecordAccess, - mapping::{self, MappingSlots}, + DealRecord, GasRecord, RecordAccess, mapping, mock::{MockCallDataContext, MockCallReturnData}, prank::Prank, }, @@ -33,7 +32,9 @@ use alloy_rpc_types::{ request::{TransactionInput, TransactionRequest}, }; use alloy_sol_types::{SolCall, SolInterface, SolValue}; -use foundry_common::{SELECTOR_LEN, TransactionMaybeSigned, evm::Breakpoints}; +use foundry_common::{ + SELECTOR_LEN, TransactionMaybeSigned, evm::Breakpoints, mapping_slots::MappingSlots, +}; use foundry_evm_core::{ InspectorExt, abi::Vm::stopExpectSafeMemoryCall, diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index c672f014df226..d05bab94850b9 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -25,12 +25,14 @@ pub mod contracts; pub mod errors; pub mod evm; pub mod fs; +pub mod mapping_slots; mod preprocessor; pub mod provider; pub mod reports; pub mod retry; pub mod selectors; pub mod serde_helpers; +pub mod slot_identifier; pub mod term; pub mod traits; pub mod transactions; diff --git a/crates/common/src/mapping_slots.rs b/crates/common/src/mapping_slots.rs new file mode 100644 index 0000000000000..2f908ce134747 --- /dev/null +++ b/crates/common/src/mapping_slots.rs @@ -0,0 +1,39 @@ +use alloy_primitives::{B256, map::B256HashMap}; + +/// Recorded mapping slots. +#[derive(Clone, Debug, Default)] +pub struct MappingSlots { + /// Holds mapping parent (slots => slots) + pub parent_slots: B256HashMap, + + /// Holds mapping key (slots => key) + pub keys: B256HashMap, + + /// Holds mapping child (slots => slots[]) + pub children: B256HashMap>, + + /// Holds the last sha3 result `sha3_result => (data_low, data_high)`, this would only record + /// when sha3 is called with `size == 0x40`, and the lower 256 bits would be stored in + /// `data_low`, higher 256 bits in `data_high`. + /// This is needed for mapping_key detect if the slot is for some mapping and record that. + pub seen_sha3: B256HashMap<(B256, B256)>, +} + +impl MappingSlots { + /// Tries to insert a mapping slot. Returns true if it was inserted. + pub fn insert(&mut self, slot: B256) -> bool { + match self.seen_sha3.get(&slot).copied() { + Some((key, parent)) => { + if self.keys.contains_key(&slot) { + return false; + } + self.keys.insert(slot, key); + self.parent_slots.insert(slot, parent); + self.children.entry(parent).or_default().push(slot); + self.insert(parent); + true + } + None => false, + } + } +} diff --git a/crates/common/src/slot_identifier.rs b/crates/common/src/slot_identifier.rs new file mode 100644 index 0000000000000..3b96e11d41c65 --- /dev/null +++ b/crates/common/src/slot_identifier.rs @@ -0,0 +1,693 @@ +//! Storage slot identification and decoding utilities for Solidity storage layouts. +//! +//! This module provides functionality to identify and decode storage slots based on +//! Solidity storage layout information from the compiler. + +use crate::mapping_slots::MappingSlots; +use alloy_dyn_abi::{DynSolType, DynSolValue}; +use alloy_primitives::{B256, U256, hex}; +use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType}; +use serde::Serialize; +use std::{str::FromStr, sync::Arc}; +use tracing::trace; + +// Constants for storage type encodings +const ENCODING_INPLACE: &str = "inplace"; +const ENCODING_MAPPING: &str = "mapping"; + +/// Information about a storage slot including its label, type, and decoded values. +#[derive(Serialize, Debug)] +pub struct SlotInfo { + /// The variable name from the storage layout. + /// + /// For top-level variables: just the variable name (e.g., "myVariable") + /// For struct members: dotted path (e.g., "myStruct.memberName") + /// For array elements: name with indices (e.g., "myArray\[0\]", "matrix\[1\]\[2\]") + /// For nested structures: full path (e.g., "outer.inner.field") + /// For mappings: base name with keys (e.g., "balances\[0x1234...\]")/ex + pub label: String, + /// The Solidity type information + #[serde(rename = "type", serialize_with = "serialize_slot_type")] + pub slot_type: StorageTypeInfo, + /// Offset within the storage slot (for packed storage) + pub offset: i64, + /// The storage slot number as a string + pub slot: String, + /// For struct members, contains nested SlotInfo for each member + /// + /// This is populated when a struct's members / fields are packed in a single slot. + #[serde(skip_serializing_if = "Option::is_none")] + pub members: Option>, + /// Decoded values (if available) - used for struct members + #[serde(skip_serializing_if = "Option::is_none")] + pub decoded: Option, + /// Decoded mapping keys (serialized as "key" for single, "keys" for multiple) + #[serde( + skip_serializing_if = "Option::is_none", + flatten, + serialize_with = "serialize_mapping_keys" + )] + pub keys: Option>, +} + +/// Wrapper type that holds both the original type label and the parsed DynSolType. +/// +/// We need both because: +/// - `label`: Used for serialization to ensure output matches user expectations +/// - `dyn_sol_type`: The parsed type used for actual value decoding +#[derive(Debug)] +pub struct StorageTypeInfo { + /// The original type label from storage layout (e.g., "uint256", "address", "mapping(address + /// => uint256)") + pub label: String, + /// The parsed dynamic Solidity type used for decoding + pub dyn_sol_type: DynSolType, +} + +impl SlotInfo { + /// Decodes a single storage value based on the slot's type information. + pub fn decode(&self, value: B256) -> Option { + // Storage values are always 32 bytes, stored as a single word + let mut actual_type = &self.slot_type.dyn_sol_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(); + } + + // Decode based on the actual type + actual_type.abi_decode(&value.0).ok() + } + + /// Decodes storage values (previous and new) and populates the decoded field. + /// For structs with members, it decodes each member individually. + pub fn decode_values(&mut self, previous_value: B256, new_value: B256) { + // If this is a struct with members, decode each member individually + if let Some(members) = &mut self.members { + for member in members.iter_mut() { + let offset = member.offset as usize; + let size = match &member.slot_type.dyn_sol_type { + DynSolType::Uint(bits) | DynSolType::Int(bits) => bits / 8, + DynSolType::Address => 20, + DynSolType::Bool => 1, + DynSolType::FixedBytes(size) => *size, + _ => 32, // Default to full word + }; + + // Extract and decode member values + let mut prev_bytes = [0u8; 32]; + let mut new_bytes = [0u8; 32]; + + if offset + size <= 32 { + // In Solidity storage, values are right-aligned + // For offset 0, we want the rightmost bytes + // For offset 16 (for a uint128), we want bytes 0-16 + // For packed storage: offset 0 is at the rightmost position + // offset 0, size 16 -> read bytes 16-32 (rightmost) + // offset 16, size 16 -> read bytes 0-16 (leftmost) + let byte_start = 32 - offset - size; + prev_bytes[32 - size..] + .copy_from_slice(&previous_value.0[byte_start..byte_start + size]); + new_bytes[32 - size..] + .copy_from_slice(&new_value.0[byte_start..byte_start + size]); + } + + // Decode the member values + if let (Ok(prev_val), Ok(new_val)) = ( + member.slot_type.dyn_sol_type.abi_decode(&prev_bytes), + member.slot_type.dyn_sol_type.abi_decode(&new_bytes), + ) { + member.decoded = + Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val }); + } + } + // For structs with members, we don't need a top-level decoded value + } else { + // For non-struct types, decode directly + if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) { + self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new }); + } + } + } +} + +/// Custom serializer for StorageTypeInfo that only outputs the label +fn serialize_slot_type(info: &StorageTypeInfo, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&info.label) +} + +/// Custom serializer for mapping keys +fn serialize_mapping_keys(keys: &Option>, serializer: S) -> Result +where + S: serde::Serializer, +{ + use serde::ser::SerializeMap; + + if let Some(keys) = keys { + let mut map = serializer.serialize_map(Some(1))?; + if keys.len() == 1 { + map.serialize_entry("key", &keys[0])?; + } else if keys.len() > 1 { + map.serialize_entry("keys", keys)?; + } + map.end() + } else { + serializer.serialize_none() + } +} + +/// Decoded storage slot values +#[derive(Debug)] +pub struct DecodedSlotValues { + /// Initial decoded storage value + pub previous_value: DynSolValue, + /// Current decoded storage value + pub 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_value(&self.previous_value))?; + state.serialize_field("newValue", &format_value(&self.new_value))?; + state.end() + } +} + +/// Storage slot identifier that uses Solidity [`StorageLayout`] to identify storage slots. +pub struct SlotIdentifier { + storage_layout: Arc, +} + +impl SlotIdentifier { + /// Creates a new SlotIdentifier with the given storage layout. + pub fn new(storage_layout: Arc) -> Self { + Self { storage_layout } + } + + /// Identifies a storage slots type using the [`StorageLayout`]. + /// + /// It can also identify whether a slot belongs to a mapping if provided with [`MappingSlots`]. + pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option { + let slot_u256 = U256::from_be_bytes(slot.0); + let slot_str = slot_u256.to_string(); + + for storage in &self.storage_layout.storage { + let storage_type = self.storage_layout.types.get(&storage.storage_type)?; + let dyn_type = DynSolType::parse(&storage_type.label).ok(); + + // Check if we're able to match on a slot from the layout i.e any of the base slots. + // This will always be the case for primitive types that fit in a single slot. + if storage.slot == slot_str + && let Some(parsed_type) = dyn_type + { + // Successfully parsed - handle arrays or simple types + let label = if let DynSolType::FixedArray(_, _) = &parsed_type { + format!("{}{}", storage.label, get_array_base_indices(&parsed_type)) + } else { + storage.label.clone() + }; + + return Some(SlotInfo { + label, + slot_type: StorageTypeInfo { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset: storage.offset, + slot: storage.slot.clone(), + members: None, + decoded: None, + keys: None, + }); + } + + // Encoding types: + if storage_type.encoding == ENCODING_INPLACE { + // Can be of type FixedArrays or Structs + // Handles the case where the accessed `slot` is maybe different from the base slot. + let array_start_slot = U256::from_str(&storage.slot).ok()?; + + if let Some(parsed_type) = dyn_type + && let DynSolType::FixedArray(_, _) = parsed_type + && let Some(slot_info) = self.handle_array_slot( + storage, + storage_type, + slot_u256, + array_start_slot, + &slot_str, + ) + { + return Some(slot_info); + } + + // If type parsing fails and the label is a struct + if is_struct(&storage_type.label) { + let struct_start_slot = U256::from_str(&storage.slot).ok()?; + if let Some(slot_info) = self.handle_struct( + &storage.label, + storage_type, + slot_u256, + struct_start_slot, + storage.offset, + &slot_str, + 0, + ) { + return Some(slot_info); + } + } + } else if storage_type.encoding == ENCODING_MAPPING + && let Some(mapping_slots) = mapping_slots + && let Some(slot_info) = + self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots) + { + return Some(slot_info); + } + } + + None + } + + /// Handles identification of array slots. + /// + /// # Arguments + /// * `storage` - The storage metadata from the layout + /// * `storage_type` - Type information for the storage slot + /// * `slot` - The target slot being identified + /// * `array_start_slot` - The starting slot of the array in storage i.e base_slot + /// * `slot_str` - String representation of the slot for output + fn handle_array_slot( + &self, + storage: &Storage, + storage_type: &StorageType, + slot: U256, + array_start_slot: U256, + slot_str: &str, + ) -> Option { + // Check if slot is within array bounds + let total_bytes = storage_type.number_of_bytes.parse::().ok()?; + let total_slots = total_bytes.div_ceil(32); + + if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) { + let parsed_type = DynSolType::parse(&storage_type.label).ok()?; + let index = (slot - array_start_slot).to::(); + // Format the array element label based on array dimensions + let label = match &parsed_type { + 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); + let col = index % (*inner_size as u64); + format!("{}[{row}][{col}]", storage.label) + } else { + // 1D array + format!("{}[{index}]", storage.label) + } + } + _ => storage.label.clone(), + }; + + return Some(SlotInfo { + label, + slot_type: StorageTypeInfo { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset: 0, + slot: slot_str.to_string(), + members: None, + decoded: None, + keys: None, + }); + } + + None + } + + /// Handles identification of struct slots. + /// + /// Recursively resolves struct members to find the exact member corresponding + /// to the target slot. Handles both single-slot (packed) and multi-slot structs. + /// + /// # Arguments + /// * `base_label` - The label/name for this struct or member + /// * `storage_type` - Type information for the storage + /// * `target_slot` - The target slot being identified + /// * `struct_start_slot` - The starting slot of this struct + /// * `offset` - Offset within the slot (for packed storage) + /// * `slot_str` - String representation of the slot for output + /// * `depth` - Current recursion depth + #[allow(clippy::too_many_arguments)] + fn handle_struct( + &self, + base_label: &str, + storage_type: &StorageType, + target_slot: U256, + struct_start_slot: U256, + offset: i64, + slot_str: &str, + depth: usize, + ) -> Option { + // Limit recursion depth to prevent stack overflow + const MAX_DEPTH: usize = 10; + if depth > MAX_DEPTH { + return None; + } + + let members = storage_type + .other + .get("members") + .and_then(|v| serde_json::from_value::>(v.clone()).ok())?; + + // If this is the exact slot we're looking for (struct's base slot) + if struct_start_slot == target_slot + // Find the member at slot offset 0 (the member that starts at this slot) + && let Some(first_member) = members.iter().find(|m| m.slot == "0") + { + let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?; + + // Check if we have a single-slot struct (all members have slot "0") + let is_single_slot = members.iter().all(|m| m.slot == "0"); + + if is_single_slot { + // Build member info for single-slot struct + let mut member_infos = Vec::new(); + for member in &members { + if let Some(member_type_info) = + self.storage_layout.types.get(&member.storage_type) + && let Some(member_type) = DynSolType::parse(&member_type_info.label).ok() + { + member_infos.push(SlotInfo { + label: member.label.clone(), + slot_type: StorageTypeInfo { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str.to_string(), + members: None, + decoded: None, + keys: None, + }); + } + } + + // Build the CustomStruct type + let struct_name = + storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label); + let prop_names: Vec = members.iter().map(|m| m.label.clone()).collect(); + let member_types: Vec = + member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect(); + + let parsed_type = DynSolType::CustomStruct { + name: struct_name.to_string(), + prop_names, + tuple: member_types, + }; + + return Some(SlotInfo { + label: base_label.to_string(), + slot_type: StorageTypeInfo { + label: storage_type.label.clone(), + dyn_sol_type: parsed_type, + }, + offset, + slot: slot_str.to_string(), + decoded: None, + members: if member_infos.is_empty() { None } else { Some(member_infos) }, + keys: None, + }); + } else { + // Multi-slot struct - return the first member. + let member_label = format!("{}.{}", base_label, first_member.label); + + // If the first member is itself a struct, recurse + if is_struct(&member_type_info.label) { + return self.handle_struct( + &member_label, + member_type_info, + target_slot, + struct_start_slot, + first_member.offset, + slot_str, + depth + 1, + ); + } + + // Return the first member as a primitive + return Some(SlotInfo { + label: member_label, + slot_type: StorageTypeInfo { + label: member_type_info.label.clone(), + dyn_sol_type: DynSolType::parse(&member_type_info.label).ok()?, + }, + offset: first_member.offset, + slot: slot_str.to_string(), + decoded: None, + members: None, + keys: None, + }); + } + } + + // Not the base slot - search through members + for member in &members { + let member_slot_offset = U256::from_str(&member.slot).ok()?; + let member_slot = struct_start_slot + member_slot_offset; + let member_type_info = self.storage_layout.types.get(&member.storage_type)?; + let member_label = format!("{}.{}", base_label, member.label); + + // If this member is a struct, recurse into it + if is_struct(&member_type_info.label) { + let slot_info = self.handle_struct( + &member_label, + member_type_info, + target_slot, + member_slot, + member.offset, + slot_str, + depth + 1, + ); + + if member_slot == target_slot || slot_info.is_some() { + return slot_info; + } + } + + if member_slot == target_slot { + // Found the exact member slot + + // Regular member + let member_type = DynSolType::parse(&member_type_info.label).ok()?; + return Some(SlotInfo { + label: member_label, + slot_type: StorageTypeInfo { + label: member_type_info.label.clone(), + dyn_sol_type: member_type, + }, + offset: member.offset, + slot: slot_str.to_string(), + members: None, + decoded: None, + keys: None, + }); + } + } + + None + } + + /// Handles identification of mapping slots. + /// + /// Identifies mapping entries by walking up the parent chain to find the base slot, + /// then decodes the keys and builds the appropriate label. + /// + /// # Arguments + /// * `storage` - The storage metadata from the layout + /// * `storage_type` - Type information for the storage + /// * `slot` - The accessed slot being identified + /// * `slot_str` - String representation of the slot for output + /// * `mapping_slots` - Tracked mapping slot accesses for key resolution + fn handle_mapping( + &self, + storage: &Storage, + storage_type: &StorageType, + slot: &B256, + slot_str: &str, + mapping_slots: &MappingSlots, + ) -> Option { + trace!( + "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}", + storage.slot, + slot, + mapping_slots.keys.contains_key(slot), + mapping_slots.parent_slots.contains_key(slot) + ); + + // Verify it's actually a mapping type + if storage_type.encoding != ENCODING_MAPPING { + return None; + } + + // Check if this slot is a known mapping entry + if !mapping_slots.keys.contains_key(slot) { + return None; + } + + // Convert storage.slot to B256 for comparison + let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?); + + // Walk up the parent chain to collect keys and validate the base slot + let mut current_slot = *slot; + let mut keys_to_decode = Vec::new(); + let mut found_base = false; + + while let Some((key, parent)) = + mapping_slots.keys.get(¤t_slot).zip(mapping_slots.parent_slots.get(¤t_slot)) + { + keys_to_decode.push(*key); + + // Check if the parent is our base storage slot + if *parent == storage_slot_b256 { + found_base = true; + break; + } + + // Move up to the parent for the next iteration + current_slot = *parent; + } + + if !found_base { + trace!("Mapping slot {} does not match any parent in chain", storage.slot); + return None; + } + + // Resolve the mapping type to get all key types and the final value type + let (key_types, value_type_label, full_type_label) = + self.resolve_mapping_type(&storage.storage_type)?; + + // Reverse keys to process from outermost to innermost + keys_to_decode.reverse(); + + // Build the label with decoded keys and collect decoded key values + let mut label = storage.label.clone(); + let mut decoded_keys = Vec::new(); + + // Decode each key using the corresponding type + for (i, key) in keys_to_decode.iter().enumerate() { + if let Some(key_type_label) = key_types.get(i) + && let Ok(sol_type) = DynSolType::parse(key_type_label) + && let Ok(decoded) = sol_type.abi_decode(&key.0) + { + let decoded_key_str = format_value(&decoded); + decoded_keys.push(decoded_key_str.clone()); + label = format!("{label}[{decoded_key_str}]"); + } else { + let hex_key = hex::encode_prefixed(key.0); + decoded_keys.push(hex_key.clone()); + label = format!("{label}[{hex_key}]"); + } + } + + // Parse the final value type for decoding + let dyn_sol_type = DynSolType::parse(&value_type_label).unwrap_or(DynSolType::Bytes); + + Some(SlotInfo { + label, + slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type }, + offset: storage.offset, + slot: slot_str.to_string(), + members: None, + decoded: None, + keys: Some(decoded_keys), + }) + } + + fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec, String, String)> { + let storage_type = self.storage_layout.types.get(type_ref)?; + + if storage_type.encoding != ENCODING_MAPPING { + // Not a mapping, return the type as-is + return Some((vec![], storage_type.label.clone(), storage_type.label.clone())); + } + + // Get key and value type references + let key_type_ref = storage_type.key.as_ref()?; + let value_type_ref = storage_type.value.as_ref()?; + + // Resolve the key type + let key_type = self.storage_layout.types.get(key_type_ref)?; + let mut key_types = vec![key_type.label.clone()]; + + // Check if the value is another mapping (nested case) + if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) { + if value_storage_type.encoding == "mapping" { + // Recursively resolve the nested mapping + let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?; + key_types.extend(nested_keys); + return Some((key_types, final_value, storage_type.label.clone())); + } else { + // Value is not a mapping, we're done + return Some(( + key_types, + value_storage_type.label.clone(), + storage_type.label.clone(), + )); + } + } + + None + } +} + +/// Returns the base indices for array types, e.g. "\[0\]\[0\]" for 2D arrays. +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 { + // Simple 1D array + "[0]".to_string() + } + } + _ => String::new(), + } +} + +/// Formats a [`DynSolValue`] as a raw string without type information and only the value itself. +pub fn format_value(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_value).collect(); + format!("[{}]", formatted.join(", ")) + } + DynSolValue::Tuple(values) => { + let formatted: Vec = values.iter().map(format_value).collect(); + format!("({})", formatted.join(", ")) + } + DynSolValue::CustomStruct { name: _, prop_names: _, tuple } => { + format_value(&DynSolValue::Tuple(tuple.clone())) + } + } +} + +/// Checks if a given type label represents a struct type. +pub fn is_struct(s: &str) -> bool { + s.starts_with("struct ") +} diff --git a/testdata/default/cheats/StateDiffMappings.t.sol b/testdata/default/cheats/StateDiffMappings.t.sol new file mode 100644 index 0000000000000..73e0c5cda66c3 --- /dev/null +++ b/testdata/default/cheats/StateDiffMappings.t.sol @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract MappingStorage { + // Simple mappings only + mapping(address => uint256) public balances; // Slot 0 + mapping(uint256 => address) public owners; // Slot 1 + mapping(bytes32 => bool) public flags; // Slot 2 + // Nested mapping + mapping(address => mapping(address => uint256)) public allowances; // Slot 3 + + function setBalance(address account, uint256 amount) public { + balances[account] = amount; + } + + function setOwner(uint256 tokenId, address owner) public { + owners[tokenId] = owner; + } + + function setFlag(bytes32 key, bool value) public { + flags[key] = value; + } + + function setAllowance(address owner, address spender, uint256 amount) public { + allowances[owner][spender] = amount; + } +} + +contract StateDiffMappingsTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + MappingStorage public mappingStorage; + + function setUp() public { + mappingStorage = new MappingStorage(); + } + + function testSimpleMappingStateDiff() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Modify a simple mapping + address testAccount = address(0x1234); + mappingStorage.setBalance(testAccount, 1000 ether); + + // Test the text format output + string memory stateDiffText = vm.getStateDiff(); + emit log_string("State diff text format:"); + emit log_string(stateDiffText); + + // Verify text format contains the mapping label + assertContains( + stateDiffText, + "balances[0x0000000000000000000000000000000000001234]", + "Text format should contain mapping label" + ); + + // Verify text format contains the value type + assertContains(stateDiffText, "uint256", "Text format should contain value type"); + + // Verify text format contains decoded values (shown with arrow) + assertContains(stateDiffText, ": 0", "Text format should contain initial value"); + assertContains( + stateDiffText, "1000000000000000000000", "Text format should contain new value (1000 ether in wei)" + ); + + // Test JSON format output + string memory json = vm.getStateDiffJson(); + emit log_string("State diff JSON (simple mapping):"); + emit log_string(json); + + // The JSON should contain the decoded mapping slot with proper label + assertContains( + json, + '"label":"balances[0x0000000000000000000000000000000000001234]"', + "JSON should contain 'balances[0x0000...1234]' label" + ); + + // Check the type is correctly identified + assertContains(json, '"type":"mapping(address => uint256)"', "JSON should contain mapping type"); + + // Check decoded values + assertContains( + json, + '"decoded":{"previousValue":"0","newValue":"1000000000000000000000"}', + "JSON should decode balance value correctly (1000 ether = 1000000000000000000000 wei)" + ); + + // Check that the key field is present for simple mapping + assertContains( + json, + '"key":"0x0000000000000000000000000000000000001234"', + "JSON should contain decoded key field for simple mapping" + ); + + // Stop recording and verify we have account accesses + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + assertTrue(accesses.length > 0, "Should have account accesses"); + + // The AccountAccess structure contains information about storage changes + // but the label and decoded values are only available in the string/JSON outputs + // We've already verified those above + } + + function testMappingWithDifferentKeyTypes() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Test uint256 key + mappingStorage.setOwner(12345, address(0x7777)); + + // Test bytes32 key + bytes32 flagKey = keccak256("test_flag"); + mappingStorage.setFlag(flagKey, true); + + // Test text format output first + string memory stateDiffText = vm.getStateDiff(); + emit log_string("State diff text format (different key types):"); + emit log_string(stateDiffText); + + // Verify text format contains decoded values for uint256 key + assertContains(stateDiffText, "owners[12345]", "Text format should contain owners mapping with decimal key"); + assertContains( + stateDiffText, + "address): 0x0000000000000000000000000000000000000000", + "Text format should contain initial address value" + ); + assertContains( + stateDiffText, "0x0000000000000000000000000000000000007777", "Text format should contain new address value" + ); + + // Verify text format contains decoded values for bytes32 key + assertContains(stateDiffText, "bool): false", "Text format should contain initial bool value"); + assertContains(stateDiffText, "true", "Text format should contain new bool value"); + + // Get state diff JSON + string memory json = vm.getStateDiffJson(); + + // Debug: log the JSON for inspection + emit log_string("State diff JSON (different key types):"); + emit log_string(json); + + // Check uint256 key mapping + assertContains(json, '"label":"owners[12345]"', "Should contain owners mapping with uint256 key"); + assertContains(json, '"type":"mapping(uint256 => address)"', "Should contain uint256=>address mapping type"); + assertContains( + json, + '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x0000000000000000000000000000000000007777"}', + "Should decode owner address correctly" + ); + + // Check bytes32 key mapping - the key will be shown as hex + assertContains(json, '"label":"flags[', "Should contain flags mapping label"); + assertContains(json, '"type":"mapping(bytes32 => bool)"', "Should contain bytes32=>bool mapping type"); + assertContains( + json, '"decoded":{"previousValue":"false","newValue":"true"}', "Should decode flag bool value correctly" + ); + + // Check that the key field is present for uint256 key mapping + assertContains(json, '"key":"12345"', "JSON should contain decoded key field for uint256 mapping"); + + // Stop recording + vm.stopAndReturnStateDiff(); + } + + function testNestedMappingStateDiff() public { + // Start recording state diffs + vm.startStateDiffRecording(); + + // Test case 1: owner1 -> spender1 + address owner1 = address(0x1111); + address spender1 = address(0x2222); + mappingStorage.setAllowance(owner1, spender1, 500 ether); + + // Test case 2: same owner (owner1) -> different spender (spender2) + address spender2 = address(0x3333); + mappingStorage.setAllowance(owner1, spender2, 750 ether); + + // Test case 3: different owner (owner2) -> different spender (spender3) + address owner2 = address(0x4444); + address spender3 = address(0x5555); + mappingStorage.setAllowance(owner2, spender3, 1000 ether); + + // Test text format output + string memory stateDiffText = vm.getStateDiff(); + emit log_string("State diff text format (nested mappings):"); + emit log_string(stateDiffText); + + // Verify text format contains nested mapping labels + assertContains( + stateDiffText, + "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]", + "Text format should contain first nested mapping label" + ); + assertContains( + stateDiffText, + "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]", + "Text format should contain second nested mapping label" + ); + // The text format shows the value type (uint256) not the full mapping type + assertContains(stateDiffText, "uint256): 0", "Text format should contain value type"); + + // Verify text format contains decoded values for nested mappings + assertContains( + stateDiffText, + "500000000000000000000", + "Text format should contain decoded value for owner1->spender1 (500 ether)" + ); + assertContains( + stateDiffText, + "750000000000000000000", + "Text format should contain decoded value for owner1->spender2 (750 ether)" + ); + assertContains( + stateDiffText, + "1000000000000000000000", + "Text format should contain decoded value for owner2->spender3 (1000 ether)" + ); + + // Test JSON format output + string memory json = vm.getStateDiffJson(); + emit log_string("State diff JSON (nested mapping - multiple entries):"); + emit log_string(json); + + // Check that all three nested mapping entries are correctly decoded + + // Entry 1: owner1 -> spender1 + assertContains( + json, + '"label":"allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]"', + "Should contain first nested mapping label (owner1 -> spender1)" + ); + assertContains( + json, '"newValue":"500000000000000000000"', "Should have correct value for owner1 -> spender1 (500 ether)" + ); + + // Entry 2: owner1 -> spender2 (same owner, different spender) + assertContains( + json, + '"label":"allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]"', + "Should contain second nested mapping label (owner1 -> spender2)" + ); + assertContains( + json, '"newValue":"750000000000000000000"', "Should have correct value for owner1 -> spender2 (750 ether)" + ); + + // Entry 3: owner2 -> spender3 (different owner) + assertContains( + json, + '"label":"allowances[0x0000000000000000000000000000000000004444][0x0000000000000000000000000000000000005555]"', + "Should contain third nested mapping label (owner2 -> spender3)" + ); + assertContains( + json, '"newValue":"1000000000000000000000"', "Should have correct value for owner2 -> spender3 (1000 ether)" + ); + + // Check the type is correctly identified for all entries + assertContains( + json, '"type":"mapping(address => mapping(address => uint256))"', "Should contain nested mapping type" + ); + + // Check that the keys field is present for nested mappings + assertContains( + json, + '"keys":["0x0000000000000000000000000000000000001111","0x0000000000000000000000000000000000002222"]', + "JSON should contain decoded keys array for owner1->spender1 nested mapping" + ); + + assertContains( + json, + '"keys":["0x0000000000000000000000000000000000001111","0x0000000000000000000000000000000000003333"]', + "JSON should contain decoded keys array for owner1->spender2 nested mapping" + ); + + assertContains( + json, + '"keys":["0x0000000000000000000000000000000000004444","0x0000000000000000000000000000000000005555"]', + "JSON should contain decoded keys array for owner2->spender3 nested mapping" + ); + + // Stop recording + 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); + } + } +} diff --git a/testdata/default/cheats/StateDiffStructTest.t.sol b/testdata/default/cheats/StateDiffStructTest.t.sol index 8c856c3789465..e4b91bc4df971 100644 --- a/testdata/default/cheats/StateDiffStructTest.t.sol +++ b/testdata/default/cheats/StateDiffStructTest.t.sol @@ -238,19 +238,19 @@ contract StateDiffStructTest is DSTest { assertContains( stateDiffJson, - '"decoded":{"previousValue":"0","newValue":"111111111"},"label":"nestedStruct.inner.value1"', + '"label":"nestedStruct.inner.value1","type":"uint256","offset":0,"slot":"4","decoded":{"previousValue":"0","newValue":"111111111"}', "Should decode inner.value1 correctly" ); assertContains( stateDiffJson, - '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000cafE"},"label":"nestedStruct.inner.addr"', + '"label":"nestedStruct.inner.addr","type":"address","offset":0,"slot":"5","decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000cafE"}', "Should decode inner.addr correctly" ); assertContains( stateDiffJson, - '"decoded":{"previousValue":"0","newValue":"222222222"},"label":"nestedStruct.inner.value2"', + '"label":"nestedStruct.inner.value2","type":"uint256","offset":0,"slot":"6","decoded":{"previousValue":"0","newValue":"222222222"}', "Should decode inner.value2 correctly" ); @@ -262,13 +262,13 @@ contract StateDiffStructTest is DSTest { assertContains( stateDiffJson, - '"decoded":{"previousValue":"0","newValue":"333333333"},"label":"nestedStruct.value"', + '"label":"nestedStruct.value","type":"uint256","offset":0,"slot":"7","decoded":{"previousValue":"0","newValue":"333333333"}', "Should decode nestedStruct.value correctly" ); assertContains( stateDiffJson, - '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000bEEF"},"label":"nestedStruct.owner"', + '"label":"nestedStruct.owner","type":"address","offset":0,"slot":"8","decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000bEEF"}', "Should decode nestedStruct.owner correctly" ); From 7460161106f9617e6eca0eb7ffaf22820df3052f Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:16:22 +0530 Subject: [PATCH 50/55] dedup assertContains test util --- .../default/cheats/StateDiffMappings.t.sol | 32 +-------------- .../cheats/StateDiffStorageLayout.t.sol | 32 +-------------- .../default/cheats/StateDiffStructTest.t.sol | 32 +-------------- .../default/cheats/StateDiffTestUtils.sol | 39 +++++++++++++++++++ 4 files changed, 45 insertions(+), 90 deletions(-) create mode 100644 testdata/default/cheats/StateDiffTestUtils.sol diff --git a/testdata/default/cheats/StateDiffMappings.t.sol b/testdata/default/cheats/StateDiffMappings.t.sol index 73e0c5cda66c3..c527f4fb468ae 100644 --- a/testdata/default/cheats/StateDiffMappings.t.sol +++ b/testdata/default/cheats/StateDiffMappings.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; +import "./StateDiffTestUtils.sol"; contract MappingStorage { // Simple mappings only @@ -29,7 +30,7 @@ contract MappingStorage { } } -contract StateDiffMappingsTest is DSTest { +contract StateDiffMappingsTest is StateDiffTestUtils { Vm constant vm = Vm(HEVM_ADDRESS); MappingStorage public mappingStorage; @@ -283,33 +284,4 @@ contract StateDiffMappingsTest is DSTest { // Stop recording 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); - } - } } diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index 961a2cd4b4363..cef8b7c7765b0 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; +import "./StateDiffTestUtils.sol"; contract SimpleStorage { uint256 public value; // Slot 0 @@ -97,7 +98,7 @@ contract TwoDArrayStorage { } } -contract StateDiffStorageLayoutTest is DSTest { +contract StateDiffStorageLayoutTest is StateDiffTestUtils { Vm constant vm = Vm(HEVM_ADDRESS); SimpleStorage simpleStorage; VariousArrays variousArrays; @@ -347,33 +348,4 @@ 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); - 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); - } - } } diff --git a/testdata/default/cheats/StateDiffStructTest.t.sol b/testdata/default/cheats/StateDiffStructTest.t.sol index e4b91bc4df971..fc5a458e3dbb0 100644 --- a/testdata/default/cheats/StateDiffStructTest.t.sol +++ b/testdata/default/cheats/StateDiffStructTest.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; +import "./StateDiffTestUtils.sol"; contract DiffTest { // slot 0 @@ -49,7 +50,7 @@ contract DiffTest { } } -contract StateDiffStructTest is DSTest { +contract StateDiffStructTest is StateDiffTestUtils { Vm constant vm = Vm(HEVM_ADDRESS); DiffTest internal test; @@ -275,33 +276,4 @@ contract StateDiffStructTest is DSTest { // Stop recording 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); - } - } } diff --git a/testdata/default/cheats/StateDiffTestUtils.sol b/testdata/default/cheats/StateDiffTestUtils.sol new file mode 100644 index 0000000000000..27ca06a076acd --- /dev/null +++ b/testdata/default/cheats/StateDiffTestUtils.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; + +/// @notice Shared test utilities for state diff tests +abstract contract StateDiffTestUtils is DSTest { + /// @notice Helper function to check if a string contains a substring + /// @param haystack The string to search in + /// @param needle The substring to search for + /// @param message The error message to display if the substring is not found + 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 5cd7b12eb0b4f6dfbbbc42a57d0c89f34862423b Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:15:02 +0530 Subject: [PATCH 51/55] 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 8e4cd275f5d98..547d2f24fb7c5 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -8,7 +8,7 @@ use crate::{ use alloy_consensus::TxEnvelope; use alloy_genesis::{Genesis, GenesisAccount}; use alloy_network::eip2718::EIP4844_TX_TYPE_ID; -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::{ From 398c5ab78cf83f9d714d618f430b6245f96d1ee5 Mon Sep 17 00:00:00 2001 From: grandizzy <38490174+grandizzy@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:56:33 +0300 Subject: [PATCH 52/55] Update crates/common/src/slot_identifier.rs Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> --- crates/common/src/slot_identifier.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/common/src/slot_identifier.rs b/crates/common/src/slot_identifier.rs index 3b96e11d41c65..467974779616b 100644 --- a/crates/common/src/slot_identifier.rs +++ b/crates/common/src/slot_identifier.rs @@ -173,6 +173,7 @@ impl Serialize for DecodedSlotValues { S: serde::Serializer, { use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?; state.serialize_field("previousValue", &format_value(&self.previous_value))?; state.serialize_field("newValue", &format_value(&self.new_value))?; From 8be596db5ea1d783b090c8ed3c09710e95fd253f Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 26 Aug 2025 11:38:12 +0300 Subject: [PATCH 53/55] Review changes: simplify get or insert, use common fmt --- crates/cheatcodes/src/evm.rs | 11 +++++----- crates/cheatcodes/src/evm/mapping.rs | 4 +--- crates/common/src/slot_identifier.rs | 32 ++++------------------------ 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 547d2f24fb7c5..9b1bb4c9efe28 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -13,7 +13,7 @@ use alloy_rlp::Decodable; use alloy_sol_types::SolValue; use foundry_common::{ fs::{read_json_file, write_json_file}, - slot_identifier::{SlotIdentifier, SlotInfo, format_value}, + slot_identifier::{SlotIdentifier, SlotInfo}, }; use foundry_evm_core::{ ContextExt, @@ -37,6 +37,7 @@ use std::{ }; mod record_debug_step; +use foundry_common::fmt::format_token_raw; use record_debug_step::{convert_call_trace_to_debug_step, flatten_call_trace}; use serde::Serialize; @@ -189,8 +190,8 @@ impl Display for AccountStateDiffs { "@ {slot} ({}, {}): {} → {}", slot_info.label, slot_info.slot_type.dyn_sol_type, - format_value(&decoded.previous_value), - format_value(&decoded.new_value) + format_token_raw(&decoded.previous_value), + format_token_raw(&decoded.new_value) )?; } else { // Have slot info but no decoded values - show raw hex values @@ -866,9 +867,7 @@ impl Cheatcode for startStateDiffRecordingCall { let Self {} = self; state.recorded_account_diffs_stack = Some(Default::default()); // Enable mapping recording to track mapping slot accesses - if state.mapping_slots.is_none() { - state.mapping_slots = Some(Default::default()); - } + state.mapping_slots.get_or_insert_default(); Ok(Default::default()) } } diff --git a/crates/cheatcodes/src/evm/mapping.rs b/crates/cheatcodes/src/evm/mapping.rs index fd8534afdb628..8b09457c21e9f 100644 --- a/crates/cheatcodes/src/evm/mapping.rs +++ b/crates/cheatcodes/src/evm/mapping.rs @@ -10,9 +10,7 @@ use revm::{ impl Cheatcode for startMappingRecordingCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self {} = self; - if state.mapping_slots.is_none() { - state.mapping_slots = Some(Default::default()); - } + state.mapping_slots.get_or_insert_default(); Ok(Default::default()) } } diff --git a/crates/common/src/slot_identifier.rs b/crates/common/src/slot_identifier.rs index 467974779616b..55fc1b814b297 100644 --- a/crates/common/src/slot_identifier.rs +++ b/crates/common/src/slot_identifier.rs @@ -6,6 +6,7 @@ use crate::mapping_slots::MappingSlots; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_primitives::{B256, U256, hex}; +use foundry_common_fmt::format_token_raw; use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType}; use serde::Serialize; use std::{str::FromStr, sync::Arc}; @@ -175,8 +176,8 @@ impl Serialize for DecodedSlotValues { use serde::ser::SerializeStruct; let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?; - state.serialize_field("previousValue", &format_value(&self.previous_value))?; - state.serialize_field("newValue", &format_value(&self.new_value))?; + state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?; + state.serialize_field("newValue", &format_token_raw(&self.new_value))?; state.end() } } @@ -586,7 +587,7 @@ impl SlotIdentifier { && let Ok(sol_type) = DynSolType::parse(key_type_label) && let Ok(decoded) = sol_type.abi_decode(&key.0) { - let decoded_key_str = format_value(&decoded); + let decoded_key_str = format_token_raw(&decoded); decoded_keys.push(decoded_key_str.clone()); label = format!("{label}[{decoded_key_str}]"); } else { @@ -663,31 +664,6 @@ fn get_array_base_indices(dyn_type: &DynSolType) -> String { } } -/// Formats a [`DynSolValue`] as a raw string without type information and only the value itself. -pub fn format_value(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_value).collect(); - format!("[{}]", formatted.join(", ")) - } - DynSolValue::Tuple(values) => { - let formatted: Vec = values.iter().map(format_value).collect(); - format!("({})", formatted.join(", ")) - } - DynSolValue::CustomStruct { name: _, prop_names: _, tuple } => { - format_value(&DynSolValue::Tuple(tuple.clone())) - } - } -} - /// Checks if a given type label represents a struct type. pub fn is_struct(s: &str) -> bool { s.starts_with("struct ") From b850bc7040249338f3a6d47ab5968b21fd806f28 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 26 Aug 2025 11:56:32 +0300 Subject: [PATCH 54/55] alloy-dyn-abi.workspace --- crates/cheatcodes/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 4c3528c9ade83..f62bde4323e71 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -24,7 +24,7 @@ foundry-evm-traces.workspace = true foundry-wallets.workspace = true forge-script-sequence.workspace = true -alloy-dyn-abi = { workspace = true, features = ["eip712"] } +alloy-dyn-abi.workspace = true alloy-evm.workspace = true alloy-json-abi.workspace = true alloy-primitives.workspace = true From fbcd265f3f24662ceb2d49d3c2ada7accfc68480 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:24:23 +0530 Subject: [PATCH 55/55] nits --- crates/common/src/mapping_slots.rs | 3 +- .../default/cheats/StateDiffMappings.t.sol | 188 +++++++----------- .../cheats/StateDiffStorageLayout.t.sol | 147 +++++++------- .../default/cheats/StateDiffStructTest.t.sol | 145 ++++++-------- .../default/cheats/StateDiffTestUtils.sol | 39 ---- 5 files changed, 209 insertions(+), 313 deletions(-) delete mode 100644 testdata/default/cheats/StateDiffTestUtils.sol diff --git a/crates/common/src/mapping_slots.rs b/crates/common/src/mapping_slots.rs index 2f908ce134747..7e5a1932b842a 100644 --- a/crates/common/src/mapping_slots.rs +++ b/crates/common/src/mapping_slots.rs @@ -24,10 +24,9 @@ impl MappingSlots { pub fn insert(&mut self, slot: B256) -> bool { match self.seen_sha3.get(&slot).copied() { Some((key, parent)) => { - if self.keys.contains_key(&slot) { + if self.keys.insert(slot, key).is_some() { return false; } - self.keys.insert(slot, key); self.parent_slots.insert(slot, parent); self.children.entry(parent).or_default().push(slot); self.insert(parent); diff --git a/testdata/default/cheats/StateDiffMappings.t.sol b/testdata/default/cheats/StateDiffMappings.t.sol index c527f4fb468ae..b4ed985791cde 100644 --- a/testdata/default/cheats/StateDiffMappings.t.sol +++ b/testdata/default/cheats/StateDiffMappings.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; -import "./StateDiffTestUtils.sol"; contract MappingStorage { // Simple mappings only @@ -30,7 +29,7 @@ contract MappingStorage { } } -contract StateDiffMappingsTest is StateDiffTestUtils { +contract StateDiffMappingsTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); MappingStorage public mappingStorage; @@ -52,20 +51,14 @@ contract StateDiffMappingsTest is StateDiffTestUtils { emit log_string(stateDiffText); // Verify text format contains the mapping label - assertContains( - stateDiffText, - "balances[0x0000000000000000000000000000000000001234]", - "Text format should contain mapping label" - ); + assertTrue(vm.contains(stateDiffText, "balances[0x0000000000000000000000000000000000001234]")); // Verify text format contains the value type - assertContains(stateDiffText, "uint256", "Text format should contain value type"); + assertTrue(vm.contains(stateDiffText, "uint256")); // Verify text format contains decoded values (shown with arrow) - assertContains(stateDiffText, ": 0", "Text format should contain initial value"); - assertContains( - stateDiffText, "1000000000000000000000", "Text format should contain new value (1000 ether in wei)" - ); + assertTrue(vm.contains(stateDiffText, ": 0")); + assertTrue(vm.contains(stateDiffText, "1000000000000000000000")); // Test JSON format output string memory json = vm.getStateDiffJson(); @@ -73,32 +66,20 @@ contract StateDiffMappingsTest is StateDiffTestUtils { emit log_string(json); // The JSON should contain the decoded mapping slot with proper label - assertContains( - json, - '"label":"balances[0x0000000000000000000000000000000000001234]"', - "JSON should contain 'balances[0x0000...1234]' label" - ); + assertTrue(vm.contains(json, '"label":"balances[0x0000000000000000000000000000000000001234]"')); // Check the type is correctly identified - assertContains(json, '"type":"mapping(address => uint256)"', "JSON should contain mapping type"); + assertTrue(vm.contains(json, '"type":"mapping(address => uint256)"')); // Check decoded values - assertContains( - json, - '"decoded":{"previousValue":"0","newValue":"1000000000000000000000"}', - "JSON should decode balance value correctly (1000 ether = 1000000000000000000000 wei)" - ); + assertTrue(vm.contains(json, '"decoded":{"previousValue":"0","newValue":"1000000000000000000000"}')); // Check that the key field is present for simple mapping - assertContains( - json, - '"key":"0x0000000000000000000000000000000000001234"', - "JSON should contain decoded key field for simple mapping" - ); + assertTrue(vm.contains(json, '"key":"0x0000000000000000000000000000000000001234"')); // Stop recording and verify we have account accesses Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); - assertTrue(accesses.length > 0, "Should have account accesses"); + assertTrue(accesses.length > 0); // The AccountAccess structure contains information about storage changes // but the label and decoded values are only available in the string/JSON outputs @@ -122,19 +103,13 @@ contract StateDiffMappingsTest is StateDiffTestUtils { emit log_string(stateDiffText); // Verify text format contains decoded values for uint256 key - assertContains(stateDiffText, "owners[12345]", "Text format should contain owners mapping with decimal key"); - assertContains( - stateDiffText, - "address): 0x0000000000000000000000000000000000000000", - "Text format should contain initial address value" - ); - assertContains( - stateDiffText, "0x0000000000000000000000000000000000007777", "Text format should contain new address value" - ); + assertTrue(vm.contains(stateDiffText, "owners[12345]")); + assertTrue(vm.contains(stateDiffText, "address): 0x0000000000000000000000000000000000000000")); + assertTrue(vm.contains(stateDiffText, "0x0000000000000000000000000000000000007777")); // Verify text format contains decoded values for bytes32 key - assertContains(stateDiffText, "bool): false", "Text format should contain initial bool value"); - assertContains(stateDiffText, "true", "Text format should contain new bool value"); + assertTrue(vm.contains(stateDiffText, "bool): false")); + assertTrue(vm.contains(stateDiffText, "true")); // Get state diff JSON string memory json = vm.getStateDiffJson(); @@ -144,23 +119,22 @@ contract StateDiffMappingsTest is StateDiffTestUtils { emit log_string(json); // Check uint256 key mapping - assertContains(json, '"label":"owners[12345]"', "Should contain owners mapping with uint256 key"); - assertContains(json, '"type":"mapping(uint256 => address)"', "Should contain uint256=>address mapping type"); - assertContains( - json, - '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x0000000000000000000000000000000000007777"}', - "Should decode owner address correctly" + assertTrue(vm.contains(json, '"label":"owners[12345]"')); + assertTrue(vm.contains(json, '"type":"mapping(uint256 => address)"')); + assertTrue( + vm.contains( + json, + '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x0000000000000000000000000000000000007777"}' + ) ); // Check bytes32 key mapping - the key will be shown as hex - assertContains(json, '"label":"flags[', "Should contain flags mapping label"); - assertContains(json, '"type":"mapping(bytes32 => bool)"', "Should contain bytes32=>bool mapping type"); - assertContains( - json, '"decoded":{"previousValue":"false","newValue":"true"}', "Should decode flag bool value correctly" - ); + assertTrue(vm.contains(json, '"label":"flags[')); + assertTrue(vm.contains(json, '"type":"mapping(bytes32 => bool)"')); + assertTrue(vm.contains(json, '"decoded":{"previousValue":"false","newValue":"true"}')); // Check that the key field is present for uint256 key mapping - assertContains(json, '"key":"12345"', "JSON should contain decoded key field for uint256 mapping"); + assertTrue(vm.contains(json, '"key":"12345"')); // Stop recording vm.stopAndReturnStateDiff(); @@ -190,35 +164,25 @@ contract StateDiffMappingsTest is StateDiffTestUtils { emit log_string(stateDiffText); // Verify text format contains nested mapping labels - assertContains( - stateDiffText, - "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]", - "Text format should contain first nested mapping label" - ); - assertContains( - stateDiffText, - "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]", - "Text format should contain second nested mapping label" + assertTrue( + vm.contains( + stateDiffText, + "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]" + ) + ); + assertTrue( + vm.contains( + stateDiffText, + "allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]" + ) ); // The text format shows the value type (uint256) not the full mapping type - assertContains(stateDiffText, "uint256): 0", "Text format should contain value type"); + assertTrue(vm.contains(stateDiffText, "uint256): 0")); // Verify text format contains decoded values for nested mappings - assertContains( - stateDiffText, - "500000000000000000000", - "Text format should contain decoded value for owner1->spender1 (500 ether)" - ); - assertContains( - stateDiffText, - "750000000000000000000", - "Text format should contain decoded value for owner1->spender2 (750 ether)" - ); - assertContains( - stateDiffText, - "1000000000000000000000", - "Text format should contain decoded value for owner2->spender3 (1000 ether)" - ); + assertTrue(vm.contains(stateDiffText, "500000000000000000000")); + assertTrue(vm.contains(stateDiffText, "750000000000000000000")); + assertTrue(vm.contains(stateDiffText, "1000000000000000000000")); // Test JSON format output string memory json = vm.getStateDiffJson(); @@ -228,57 +192,55 @@ contract StateDiffMappingsTest is StateDiffTestUtils { // Check that all three nested mapping entries are correctly decoded // Entry 1: owner1 -> spender1 - assertContains( - json, - '"label":"allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]"', - "Should contain first nested mapping label (owner1 -> spender1)" - ); - assertContains( - json, '"newValue":"500000000000000000000"', "Should have correct value for owner1 -> spender1 (500 ether)" + assertTrue( + vm.contains( + json, + '"label":"allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000002222]"' + ) ); + assertTrue(vm.contains(json, '"newValue":"500000000000000000000"')); // Entry 2: owner1 -> spender2 (same owner, different spender) - assertContains( - json, - '"label":"allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]"', - "Should contain second nested mapping label (owner1 -> spender2)" - ); - assertContains( - json, '"newValue":"750000000000000000000"', "Should have correct value for owner1 -> spender2 (750 ether)" + assertTrue( + vm.contains( + json, + '"label":"allowances[0x0000000000000000000000000000000000001111][0x0000000000000000000000000000000000003333]"' + ) ); + assertTrue(vm.contains(json, '"newValue":"750000000000000000000"')); // Entry 3: owner2 -> spender3 (different owner) - assertContains( - json, - '"label":"allowances[0x0000000000000000000000000000000000004444][0x0000000000000000000000000000000000005555]"', - "Should contain third nested mapping label (owner2 -> spender3)" - ); - assertContains( - json, '"newValue":"1000000000000000000000"', "Should have correct value for owner2 -> spender3 (1000 ether)" + assertTrue( + vm.contains( + json, + '"label":"allowances[0x0000000000000000000000000000000000004444][0x0000000000000000000000000000000000005555]"' + ) ); + assertTrue(vm.contains(json, '"newValue":"1000000000000000000000"')); // Check the type is correctly identified for all entries - assertContains( - json, '"type":"mapping(address => mapping(address => uint256))"', "Should contain nested mapping type" - ); + assertTrue(vm.contains(json, '"type":"mapping(address => mapping(address => uint256))"')); // Check that the keys field is present for nested mappings - assertContains( - json, - '"keys":["0x0000000000000000000000000000000000001111","0x0000000000000000000000000000000000002222"]', - "JSON should contain decoded keys array for owner1->spender1 nested mapping" + assertTrue( + vm.contains( + json, + '"keys":["0x0000000000000000000000000000000000001111","0x0000000000000000000000000000000000002222"]' + ) ); - assertContains( - json, - '"keys":["0x0000000000000000000000000000000000001111","0x0000000000000000000000000000000000003333"]', - "JSON should contain decoded keys array for owner1->spender2 nested mapping" + assertTrue( + vm.contains( + json, + '"keys":["0x0000000000000000000000000000000000001111","0x0000000000000000000000000000000000003333"]' + ) ); - assertContains( - json, - '"keys":["0x0000000000000000000000000000000000004444","0x0000000000000000000000000000000000005555"]', - "JSON should contain decoded keys array for owner2->spender3 nested mapping" + assertTrue( + vm.contains( + json, + '"keys":["0x0000000000000000000000000000000000004444","0x0000000000000000000000000000000000005555"]' + ) ); // Stop recording diff --git a/testdata/default/cheats/StateDiffStorageLayout.t.sol b/testdata/default/cheats/StateDiffStorageLayout.t.sol index cef8b7c7765b0..027e3d8567098 100644 --- a/testdata/default/cheats/StateDiffStorageLayout.t.sol +++ b/testdata/default/cheats/StateDiffStorageLayout.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; -import "./StateDiffTestUtils.sol"; contract SimpleStorage { uint256 public value; // Slot 0 @@ -98,7 +97,7 @@ contract TwoDArrayStorage { } } -contract StateDiffStorageLayoutTest is StateDiffTestUtils { +contract StateDiffStorageLayoutTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); SimpleStorage simpleStorage; VariousArrays variousArrays; @@ -127,31 +126,31 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { // 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"); + assertTrue(vm.contains(stateDiffJson, '"label":"value"')); + assertTrue(vm.contains(stateDiffJson, '"label":"owner"')); + assertTrue(vm.contains(stateDiffJson, '"label":"values[0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"values[1]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"values[2]"')); - 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"); + assertTrue(vm.contains(stateDiffJson, '"type":"uint256"')); + assertTrue(vm.contains(stateDiffJson, '"type":"address"')); + assertTrue(vm.contains(stateDiffJson, '"type":"uint256[3]"')); // Check for decoded values - assertContains(stateDiffJson, '"decoded":', "Should contain decoded values"); + assertTrue(vm.contains(stateDiffJson, '"decoded":')); // 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"); + assertTrue(vm.contains(stateDiffJson, '"decoded":{"previousValue":"0","newValue":"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"); + assertTrue(vm.contains(stateDiffJson, '"newValue":"100"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"200"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"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"); + assertTrue(accesses.length >= 3); // Verify storage accesses for SimpleStorage bool foundValueSlot = false; @@ -173,11 +172,11 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { } } - 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])"); + assertTrue(foundValueSlot); + assertTrue(foundOwnerSlot); + assertTrue(foundValuesSlot0); + assertTrue(foundValuesSlot1); + assertTrue(foundValuesSlot2); } function testVariousArrayTypes() public { @@ -194,37 +193,33 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { 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"); + assertTrue(vm.contains(stateDiffJson, '"label":"numbers[0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"numbers[1]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"numbers[2]"')); - assertContains(stateDiffJson, '"label":"addresses[0]"', "Should contain 'addresses[0]' label"); - assertContains(stateDiffJson, '"label":"addresses[1]"', "Should contain 'addresses[1]' label"); + assertTrue(vm.contains(stateDiffJson, '"label":"addresses[0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"addresses[1]"')); - assertContains(stateDiffJson, '"label":"flags[0]"', "Should contain 'flags[0]' label"); + assertTrue(vm.contains(stateDiffJson, '"label":"flags[0]"')); - assertContains(stateDiffJson, '"label":"hashes[0]"', "Should contain 'hashes[0]' label"); - assertContains(stateDiffJson, '"label":"hashes[1]"', "Should contain 'hashes[1]' label"); + assertTrue(vm.contains(stateDiffJson, '"label":"hashes[0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"hashes[1]"')); // 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"); + assertTrue(vm.contains(stateDiffJson, '"type":"uint256[3]"')); + assertTrue(vm.contains(stateDiffJson, '"type":"address[2]"')); + assertTrue(vm.contains(stateDiffJson, '"type":"bool[5]"')); + assertTrue(vm.contains(stateDiffJson, '"type":"bytes32[2]"')); // Check decoded values - assertContains(stateDiffJson, '"decoded":', "Should contain decoded values"); + assertTrue(vm.contains(stateDiffJson, '"decoded":')); // Check addresses are decoded as raw hex strings - assertContains( - stateDiffJson, '"newValue":"0x0000000000000000000000000000000000000001"', "Should decode address 1" - ); - assertContains( - stateDiffJson, '"newValue":"0x0000000000000000000000000000000000000002"', "Should decode address 2" - ); + assertTrue(vm.contains(stateDiffJson, '"newValue":"0x0000000000000000000000000000000000000001"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"0x0000000000000000000000000000000000000002"')); // Stop recording and verify account accesses Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); - assertTrue(accesses.length > 0, "Should have account accesses"); + assertTrue(accesses.length > 0); } function testStateDiffJsonFormat() public { @@ -238,12 +233,12 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { 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"); + assertTrue(vm.contains(stateDiffJson, '"previousValue":')); + assertTrue(vm.contains(stateDiffJson, '"newValue":')); + assertTrue(vm.contains(stateDiffJson, '"label":')); + assertTrue(vm.contains(stateDiffJson, '"type":')); + assertTrue(vm.contains(stateDiffJson, '"offset":')); + assertTrue(vm.contains(stateDiffJson, '"slot":')); vm.stopAndReturnStateDiff(); } @@ -263,24 +258,24 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { 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"); + assertTrue(vm.contains(stateDiffJson, '"label":"matrix[0][0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"matrix[0][1]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"matrix[0][2]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"matrix[1][0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"matrix[1][1]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"matrix[1][2]"')); // Check that we have the right type - assertContains(stateDiffJson, '"type":"uint256[3][2]"', "Should contain 2D array type"); + assertTrue(vm.contains(stateDiffJson, '"type":"uint256[3][2]"')); // 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"); + assertTrue(vm.contains(stateDiffJson, '"decoded":')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"100"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"101"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"102"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"200"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"201"')); + assertTrue(vm.contains(stateDiffJson, '"newValue":"202"')); vm.stopAndReturnStateDiff(); } @@ -303,20 +298,20 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { 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"); + assertTrue(vm.contains(stateDiffJson, '"type":"address[2][3]"')); + assertTrue(vm.contains(stateDiffJson, '"type":"bytes32[2][4]"')); // 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"); + assertTrue(vm.contains(stateDiffJson, '"label":"addresses2D[0][0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"addresses2D[0][1]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"addresses2D[1][0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"addresses2D[2][1]"')); // 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"); + assertTrue(vm.contains(stateDiffJson, '"label":"data2D[0][0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"data2D[0][1]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"data2D[1][0]"')); + assertTrue(vm.contains(stateDiffJson, '"label":"data2D[1][1]"')); vm.stopAndReturnStateDiff(); } @@ -336,15 +331,15 @@ contract StateDiffStorageLayoutTest is StateDiffTestUtils { // 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"); + assertTrue(vm.contains(stateDiff, unicode"0 → 42")); // For addresses, should show decoded address format - assertContains(stateDiff, "0x000000000000000000000000000000000000bEEF", "Should show decoded address"); + assertTrue(vm.contains(stateDiff, "0x000000000000000000000000000000000000bEEF")); // 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"); + assertTrue(vm.contains(stateDiff, unicode"0 → 100")); + assertTrue(vm.contains(stateDiff, unicode"0 → 200")); + assertTrue(vm.contains(stateDiff, unicode"0 → 300")); vm.stopAndReturnStateDiff(); } diff --git a/testdata/default/cheats/StateDiffStructTest.t.sol b/testdata/default/cheats/StateDiffStructTest.t.sol index fc5a458e3dbb0..a0b29f156c673 100644 --- a/testdata/default/cheats/StateDiffStructTest.t.sol +++ b/testdata/default/cheats/StateDiffStructTest.t.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; -import "./StateDiffTestUtils.sol"; contract DiffTest { // slot 0 @@ -50,7 +49,7 @@ contract DiffTest { } } -contract StateDiffStructTest is StateDiffTestUtils { +contract StateDiffStructTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); DiffTest internal test; @@ -73,35 +72,37 @@ contract StateDiffStructTest is StateDiffTestUtils { emit log_string(stateDiffJson); // Check that the struct is properly labeled - assertContains(stateDiffJson, '"label":"testStruct"', "Should contain 'testStruct' label"); + assertTrue(vm.contains(stateDiffJson, '"label":"testStruct"')); // Check that the type is correctly identified as a struct - assertContains(stateDiffJson, '"type":"struct DiffTest.TestStruct"', "Should contain struct type"); + assertTrue(vm.contains(stateDiffJson, '"type":"struct DiffTest.TestStruct"')); // Check for members field - structs have members with individual decoded values - assertContains(stateDiffJson, '"members":', "Should contain members field for struct"); + assertTrue(vm.contains(stateDiffJson, '"members":')); // Check that member 'a' is properly decoded - assertContains(stateDiffJson, '"label":"a"', "Should contain member 'a' label"); - assertContains(stateDiffJson, '"type":"uint128"', "Should contain uint128 type for members"); + assertTrue(vm.contains(stateDiffJson, '"label":"a"')); + assertTrue(vm.contains(stateDiffJson, '"type":"uint128"')); // Check that member 'b' is properly decoded - assertContains(stateDiffJson, '"label":"b"', "Should contain member 'b' label"); + assertTrue(vm.contains(stateDiffJson, '"label":"b"')); // The members should have decoded values // Check specific decoded values for each member in the members array // Member 'a' at offset 0 should have previous value 0 and new value 1 - assertContains( - stateDiffJson, - '{"label":"a","type":"uint128","offset":0,"slot":"0","decoded":{"previousValue":"0","newValue":"1"}}', - "Member 'a' should be decoded with previous=0, new=1" + assertTrue( + vm.contains( + stateDiffJson, + '{"label":"a","type":"uint128","offset":0,"slot":"0","decoded":{"previousValue":"0","newValue":"1"}}' + ) ); // Member 'b' at offset 16 should have previous value 0 and new value 2 - assertContains( - stateDiffJson, - '{"label":"b","type":"uint128","offset":16,"slot":"0","decoded":{"previousValue":"0","newValue":"2"}}', - "Member 'b' should be decoded with previous=0, new=2" + assertTrue( + vm.contains( + stateDiffJson, + '{"label":"b","type":"uint128","offset":16,"slot":"0","decoded":{"previousValue":"0","newValue":"2"}}' + ) ); // Verify the raw storage values are correct @@ -109,15 +110,11 @@ contract StateDiffStructTest is StateDiffTestUtils { // So the value 0x0000000000000000000200000000000000000000000000000001 represents: // - First 16 bytes (a): 0x0000000000000000000000000000000001 = 1 // - Last 16 bytes (b): 0x0000000000000000000000000000000002 = 2 - assertContains( - stateDiffJson, - '"0x0000000000000000000000000000000200000000000000000000000000000001"', - "Should contain the correct packed storage value" - ); + assertTrue(vm.contains(stateDiffJson, '"0x0000000000000000000000000000000200000000000000000000000000000001"')); // Stop recording and verify we get the expected account accesses Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); - assertTrue(accesses.length > 0, "Should have account accesses"); + assertTrue(accesses.length > 0); // Find the storage access for our struct bool foundStructAccess = false; @@ -128,18 +125,16 @@ contract StateDiffStructTest is StateDiffTestUtils { if (access.slot == bytes32(uint256(0)) && access.isWrite) { foundStructAccess = true; // Verify the storage values - assertEq(access.previousValue, bytes32(uint256(0)), "Previous value should be 0"); + assertEq(access.previousValue, bytes32(uint256(0))); assertEq( - access.newValue, - bytes32(uint256(0x0000000000000000000200000000000000000000000000000001)), - "New value should pack a=1 and b=2" + access.newValue, bytes32(uint256(0x0000000000000000000200000000000000000000000000000001)) ); } } } } - assertTrue(foundStructAccess, "Should have found struct storage access"); + assertTrue(foundStructAccess); } function testMultiSlotStruct() public { @@ -161,9 +156,7 @@ contract StateDiffStructTest is StateDiffTestUtils { emit log_string(stateDiffJson); // Check that the struct's first member is properly labeled - assertContains( - stateDiffJson, '"label":"multiSlotStruct.value1"', "Should contain 'multiSlotStruct.value1' label" - ); + assertTrue(vm.contains(stateDiffJson, '"label":"multiSlotStruct.value1"')); // For multi-slot structs, the base slot now shows the first member's type // The struct type itself is not shown since we decode the first member directly @@ -173,45 +166,30 @@ contract StateDiffStructTest is StateDiffTestUtils { // Check that each member slot is properly labeled // Note: slot 1 now shows multiSlotStruct.value1 since it's the first member - assertContains( - stateDiffJson, - '"label":"multiSlotStruct.value1"', - "Should contain multiSlotStruct.value1 label for first slot" - ); - assertContains(stateDiffJson, '"label":"multiSlotStruct.addr"', "Should contain member 'addr' label"); - assertContains(stateDiffJson, '"label":"multiSlotStruct.value2"', "Should contain member 'value2' label"); + assertTrue(vm.contains(stateDiffJson, '"label":"multiSlotStruct.value1"')); + assertTrue(vm.contains(stateDiffJson, '"label":"multiSlotStruct.addr"')); + assertTrue(vm.contains(stateDiffJson, '"label":"multiSlotStruct.value2"')); // Check member types - assertContains(stateDiffJson, '"type":"uint256"', "Should contain uint256 type"); - assertContains(stateDiffJson, '"type":"address"', "Should contain address type"); + assertTrue(vm.contains(stateDiffJson, '"type":"uint256"')); + assertTrue(vm.contains(stateDiffJson, '"type":"address"')); // Check that value1 is properly decoded from slot 1 - assertContains( - stateDiffJson, - '"decoded":{"previousValue":"0","newValue":"123456789"}', - "value1 should be decoded from slot 1" - ); + assertTrue(vm.contains(stateDiffJson, '"decoded":{"previousValue":"0","newValue":"123456789"}')); // Also verify the raw hex value - assertContains( - stateDiffJson, - "0x00000000000000000000000000000000000000000000000000000000075bcd15", - "Slot 1 should contain value1 in hex" - ); + assertTrue(vm.contains(stateDiffJson, "0x00000000000000000000000000000000000000000000000000000000075bcd15")); // Slot 2 should have the address decoded - assertContains( - stateDiffJson, - '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x00000000000000000000000000000000DeaDBeef"}', - "Address should be decoded from slot 2" + assertTrue( + vm.contains( + stateDiffJson, + '"decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x00000000000000000000000000000000DeaDBeef"}' + ) ); // Slot 3 should have value2 decoded - assertContains( - stateDiffJson, - '"decoded":{"previousValue":"0","newValue":"987654321"}', - "Value2 should be decoded from slot 3" - ); + assertTrue(vm.contains(stateDiffJson, '"decoded":{"previousValue":"0","newValue":"987654321"}')); // Stop recording vm.stopAndReturnStateDiff(); @@ -237,40 +215,41 @@ contract StateDiffStructTest is StateDiffTestUtils { emit log_string("State diff JSON (testdata):"); emit log_string(stateDiffJson); - assertContains( - stateDiffJson, - '"label":"nestedStruct.inner.value1","type":"uint256","offset":0,"slot":"4","decoded":{"previousValue":"0","newValue":"111111111"}', - "Should decode inner.value1 correctly" + assertTrue( + vm.contains( + stateDiffJson, + '"label":"nestedStruct.inner.value1","type":"uint256","offset":0,"slot":"4","decoded":{"previousValue":"0","newValue":"111111111"}' + ) ); - assertContains( - stateDiffJson, - '"label":"nestedStruct.inner.addr","type":"address","offset":0,"slot":"5","decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000cafE"}', - "Should decode inner.addr correctly" + assertTrue( + vm.contains( + stateDiffJson, + '"label":"nestedStruct.inner.addr","type":"address","offset":0,"slot":"5","decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000cafE"}' + ) ); - assertContains( - stateDiffJson, - '"label":"nestedStruct.inner.value2","type":"uint256","offset":0,"slot":"6","decoded":{"previousValue":"0","newValue":"222222222"}', - "Should decode inner.value2 correctly" + assertTrue( + vm.contains( + stateDiffJson, + '"label":"nestedStruct.inner.value2","type":"uint256","offset":0,"slot":"6","decoded":{"previousValue":"0","newValue":"222222222"}' + ) ); - assertContains( - stateDiffJson, - "0x00000000000000000000000000000000000000000000000000000000069f6bc7", - "Slot 4 should contain inner.value1 in hex" - ); + assertTrue(vm.contains(stateDiffJson, "0x00000000000000000000000000000000000000000000000000000000069f6bc7")); - assertContains( - stateDiffJson, - '"label":"nestedStruct.value","type":"uint256","offset":0,"slot":"7","decoded":{"previousValue":"0","newValue":"333333333"}', - "Should decode nestedStruct.value correctly" + assertTrue( + vm.contains( + stateDiffJson, + '"label":"nestedStruct.value","type":"uint256","offset":0,"slot":"7","decoded":{"previousValue":"0","newValue":"333333333"}' + ) ); - assertContains( - stateDiffJson, - '"label":"nestedStruct.owner","type":"address","offset":0,"slot":"8","decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000bEEF"}', - "Should decode nestedStruct.owner correctly" + assertTrue( + vm.contains( + stateDiffJson, + '"label":"nestedStruct.owner","type":"address","offset":0,"slot":"8","decoded":{"previousValue":"0x0000000000000000000000000000000000000000","newValue":"0x000000000000000000000000000000000000bEEF"}' + ) ); // Stop recording diff --git a/testdata/default/cheats/StateDiffTestUtils.sol b/testdata/default/cheats/StateDiffTestUtils.sol deleted file mode 100644 index 27ca06a076acd..0000000000000 --- a/testdata/default/cheats/StateDiffTestUtils.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; - -import "ds-test/test.sol"; - -/// @notice Shared test utilities for state diff tests -abstract contract StateDiffTestUtils is DSTest { - /// @notice Helper function to check if a string contains a substring - /// @param haystack The string to search in - /// @param needle The substring to search for - /// @param message The error message to display if the substring is not found - 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); - } - } -}