@@ -6,11 +6,13 @@ use crate::{
66 inspector:: { Ecx , RecordDebugStepInfo } ,
77} ;
88use alloy_consensus:: TxEnvelope ;
9+ use alloy_dyn_abi:: { DynSolType , DynSolValue } ;
910use alloy_genesis:: { Genesis , GenesisAccount } ;
10- use alloy_primitives:: { Address , B256 , U256 , map:: HashMap } ;
11+ use alloy_primitives:: { Address , B256 , U256 , hex , map:: HashMap } ;
1112use alloy_rlp:: Decodable ;
1213use alloy_sol_types:: SolValue ;
1314use foundry_common:: fs:: { read_json_file, write_json_file} ;
15+ use foundry_compilers:: artifacts:: StorageLayout ;
1416use foundry_evm_core:: {
1517 ContextExt ,
1618 backend:: { DatabaseExt , RevertStateSnapshotAction } ,
@@ -30,6 +32,7 @@ use std::{
3032 collections:: { BTreeMap , HashSet , btree_map:: Entry } ,
3133 fmt:: Display ,
3234 path:: Path ,
35+ str:: FromStr ,
3336} ;
3437
3538mod record_debug_step;
@@ -103,6 +106,58 @@ struct SlotStateDiff {
103106 previous_value : B256 ,
104107 /// Current storage value.
105108 new_value : B256 ,
109+ /// Decoded values according to the Solidity type (e.g., uint256, address).
110+ /// Only present when storage layout is available and decoding succeeds.
111+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
112+ decoded : Option < DecodedSlotValues > ,
113+
114+ /// Storage layout metadata (variable name, type, offset).
115+ /// Only present when contract has storage layout output.
116+ #[ serde( skip_serializing_if = "Option::is_none" , flatten) ]
117+ slot_info : Option < SlotInfo > ,
118+ }
119+
120+ /// Storage slot metadata from the contract's storage layout.
121+ #[ derive( Serialize , Debug ) ]
122+ struct SlotInfo {
123+ /// Variable name (e.g., "owner", "values\[0\]", "config.maxSize").
124+ label : String ,
125+ /// Solidity type, serialized as string (e.g., "uint256", "address").
126+ #[ serde( rename = "type" , serialize_with = "serialize_dyn_sol_type" ) ]
127+ dyn_sol_type : DynSolType ,
128+ /// Byte offset within the 32-byte slot (0 for full slot, 0-31 for packed).
129+ offset : i64 ,
130+ /// Storage slot number as decimal string.
131+ slot : String ,
132+ }
133+
134+ fn serialize_dyn_sol_type < S > ( dyn_type : & DynSolType , serializer : S ) -> Result < S :: Ok , S :: Error >
135+ where
136+ S : serde:: Serializer ,
137+ {
138+ serializer. serialize_str ( & dyn_type. to_string ( ) )
139+ }
140+
141+ /// Decoded storage values showing before and after states.
142+ #[ derive( Debug ) ]
143+ struct DecodedSlotValues {
144+ /// Decoded value before the state change.
145+ previous_value : DynSolValue ,
146+ /// Decoded value after the state change.
147+ new_value : DynSolValue ,
148+ }
149+
150+ impl Serialize for DecodedSlotValues {
151+ fn serialize < S > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error >
152+ where
153+ S : serde:: Serializer ,
154+ {
155+ use serde:: ser:: SerializeStruct ;
156+ let mut state = serializer. serialize_struct ( "DecodedSlotValues" , 2 ) ?;
157+ state. serialize_field ( "previousValue" , & format_dyn_sol_value_raw ( & self . previous_value ) ) ?;
158+ state. serialize_field ( "newValue" , & format_dyn_sol_value_raw ( & self . new_value ) ) ?;
159+ state. end ( )
160+ }
106161}
107162
108163/// Balance diff info.
@@ -170,11 +225,38 @@ impl Display for AccountStateDiffs {
170225 if !& self . state_diff . is_empty ( ) {
171226 writeln ! ( f, "- state diff:" ) ?;
172227 for ( slot, slot_changes) in & self . state_diff {
173- writeln ! (
174- f,
175- "@ {slot}: {} → {}" ,
176- slot_changes. previous_value, slot_changes. new_value
177- ) ?;
228+ match ( & slot_changes. slot_info , & slot_changes. decoded ) {
229+ ( Some ( slot_info) , Some ( decoded) ) => {
230+ // Have both slot info and decoded values - only show decoded values
231+ writeln ! (
232+ f,
233+ "@ {slot} ({}, {}): {} → {}" ,
234+ slot_info. label,
235+ slot_info. dyn_sol_type,
236+ format_dyn_sol_value_raw( & decoded. previous_value) ,
237+ format_dyn_sol_value_raw( & decoded. new_value)
238+ ) ?;
239+ }
240+ ( Some ( slot_info) , None ) => {
241+ // Have slot info but no decoded values - show raw hex values
242+ writeln ! (
243+ f,
244+ "@ {slot} ({}, {}): {} → {}" ,
245+ slot_info. label,
246+ slot_info. dyn_sol_type,
247+ slot_changes. previous_value,
248+ slot_changes. new_value
249+ ) ?;
250+ }
251+ _ => {
252+ // No slot info - show raw hex values
253+ writeln ! (
254+ f,
255+ "@ {slot}: {} → {}" ,
256+ slot_changes. previous_value, slot_changes. new_value
257+ ) ?;
258+ }
259+ }
178260 }
179261 }
180262
@@ -1257,12 +1339,20 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap<Address, AccountSt
12571339 }
12581340 }
12591341
1260- // Look up contract names for all addresses
1342+ // Look up contract names and storage layouts for all addresses
12611343 let mut contract_names = HashMap :: new ( ) ;
1344+ let mut storage_layouts = HashMap :: new ( ) ;
12621345 for address in addresses_to_lookup {
12631346 if let Some ( name) = get_contract_name ( ccx, address) {
12641347 contract_names. insert ( address, name) ;
12651348 }
1349+
1350+ // Also get storage layout if available
1351+ if let Some ( ( _artifact_id, contract_data) ) = get_contract_data ( ccx, address)
1352+ && let Some ( storage_layout) = & contract_data. storage_layout
1353+ {
1354+ storage_layouts. insert ( address, storage_layout. clone ( ) ) ;
1355+ }
12661356 }
12671357
12681358 // Now process the records
@@ -1331,13 +1421,44 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap<Address, AccountSt
13311421 // Update state diff. Do not overwrite the initial value if already set.
13321422 match account_diff. state_diff . entry ( storage_access. slot ) {
13331423 Entry :: Vacant ( slot_state_diff) => {
1424+ // Get storage layout info for this slot
1425+ let slot_info = storage_layouts
1426+ . get ( & storage_access. account )
1427+ . and_then ( |layout| get_slot_info ( layout, & storage_access. slot ) ) ;
1428+
1429+ // Try to decode values if we have slot info
1430+ let decoded = slot_info. as_ref ( ) . and_then ( |info| {
1431+ let prev = decode_storage_value (
1432+ storage_access. previousValue ,
1433+ & info. dyn_sol_type ,
1434+ ) ?;
1435+ let new = decode_storage_value (
1436+ storage_access. newValue ,
1437+ & info. dyn_sol_type ,
1438+ ) ?;
1439+ Some ( DecodedSlotValues { previous_value : prev, new_value : new } )
1440+ } ) ;
1441+
13341442 slot_state_diff. insert ( SlotStateDiff {
13351443 previous_value : storage_access. previousValue ,
13361444 new_value : storage_access. newValue ,
1445+ decoded,
1446+ slot_info,
13371447 } ) ;
13381448 }
13391449 Entry :: Occupied ( mut slot_state_diff) => {
1340- slot_state_diff. get_mut ( ) . new_value = storage_access. newValue ;
1450+ let entry = slot_state_diff. get_mut ( ) ;
1451+ entry. new_value = storage_access. newValue ;
1452+
1453+ if let Some ( slot_info) = & entry. slot_info
1454+ && let Some ( ref mut decoded) = entry. decoded
1455+ && let Some ( new_value) = decode_storage_value (
1456+ storage_access. newValue ,
1457+ & slot_info. dyn_sol_type ,
1458+ )
1459+ {
1460+ decoded. new_value = new_value;
1461+ }
13411462 }
13421463 }
13431464 }
@@ -1375,6 +1496,158 @@ fn get_contract_name(ccx: &mut CheatsCtxt, address: Address) -> Option<String> {
13751496 None
13761497}
13771498
1499+ /// Helper function to get the contract data from the deployed code at an address.
1500+ fn get_contract_data < ' a > (
1501+ ccx : & ' a mut CheatsCtxt ,
1502+ address : Address ,
1503+ ) -> Option < ( & ' a foundry_compilers:: ArtifactId , & ' a foundry_common:: contracts:: ContractData ) > {
1504+ // Check if we have available artifacts to match against
1505+ let artifacts = ccx. state . config . available_artifacts . as_ref ( ) ?;
1506+
1507+ // Try to load the account and get its code
1508+ let account = ccx. ecx . journaled_state . load_account ( address) . ok ( ) ?;
1509+ let code = account. info . code . as_ref ( ) ?;
1510+
1511+ // Skip if code is empty
1512+ if code. is_empty ( ) {
1513+ return None ;
1514+ }
1515+
1516+ // Try to find the artifact by deployed code
1517+ let code_bytes = code. original_bytes ( ) ;
1518+ if let Some ( result) = artifacts. find_by_deployed_code_exact ( & code_bytes) {
1519+ return Some ( result) ;
1520+ }
1521+
1522+ // Fallback to fuzzy matching if exact match fails
1523+ artifacts. find_by_deployed_code ( & code_bytes)
1524+ }
1525+
1526+ /// Gets storage layout info for a specific slot.
1527+ fn get_slot_info ( storage_layout : & StorageLayout , slot : & B256 ) -> Option < SlotInfo > {
1528+ let slot = U256 :: from_be_bytes ( slot. 0 ) ;
1529+ let slot_str = slot. to_string ( ) ;
1530+
1531+ for storage in & storage_layout. storage {
1532+ let base_slot = U256 :: from_str ( & storage. slot ) . ok ( ) ?;
1533+ let storage_type = storage_layout. types . get ( & storage. storage_type ) ?;
1534+ let dyn_type = DynSolType :: parse ( & storage_type. label ) . ok ( ) ?;
1535+
1536+ // Check for exact slot match
1537+ if storage. slot == slot_str {
1538+ let label = match & dyn_type {
1539+ DynSolType :: FixedArray ( _, _) => {
1540+ // For arrays, label the base slot with indices
1541+ format ! ( "{}{}" , storage. label, get_array_base_indices( & dyn_type) )
1542+ }
1543+ _ => storage. label . clone ( ) ,
1544+ } ;
1545+
1546+ return Some ( SlotInfo {
1547+ label,
1548+ dyn_sol_type : dyn_type,
1549+ offset : storage. offset ,
1550+ slot : storage. slot . clone ( ) ,
1551+ } ) ;
1552+ }
1553+
1554+ // Check if slot is part of a static array
1555+ if let DynSolType :: FixedArray ( _, _) = & dyn_type
1556+ && let Ok ( total_bytes) = storage_type. number_of_bytes . parse :: < u64 > ( )
1557+ {
1558+ let total_slots = total_bytes. div_ceil ( 32 ) ;
1559+
1560+ // Check if slot is within array range
1561+ if slot > base_slot && slot < base_slot + U256 :: from ( total_slots) {
1562+ let index = ( slot - base_slot) . to :: < u64 > ( ) ;
1563+ let label = format_array_element_label ( & storage. label , & dyn_type, index) ;
1564+
1565+ return Some ( SlotInfo {
1566+ label,
1567+ dyn_sol_type : dyn_type,
1568+ offset : 0 ,
1569+ slot : slot. to_string ( ) ,
1570+ } ) ;
1571+ }
1572+ }
1573+ }
1574+
1575+ None
1576+ }
1577+
1578+ /// Returns the base index [\0\] or [\0\][\0\] for a fixed array type depending on the dimensions.
1579+ fn get_array_base_indices ( dyn_type : & DynSolType ) -> String {
1580+ match dyn_type {
1581+ DynSolType :: FixedArray ( inner, _) => {
1582+ if let DynSolType :: FixedArray ( _, _) = inner. as_ref ( ) {
1583+ // Nested array (2D or higher)
1584+ format ! ( "[0]{}" , get_array_base_indices( inner) )
1585+ } else {
1586+ // Simple 1D array
1587+ "[0]" . to_string ( )
1588+ }
1589+ }
1590+ _ => String :: new ( ) ,
1591+ }
1592+ }
1593+
1594+ /// Helper function to format an array element label given its index
1595+ fn format_array_element_label ( base_label : & str , dyn_type : & DynSolType , index : u64 ) -> String {
1596+ match dyn_type {
1597+ DynSolType :: FixedArray ( inner, _) => {
1598+ if let DynSolType :: FixedArray ( _, inner_size) = inner. as_ref ( ) {
1599+ // 2D array: calculate row and column
1600+ let row = index / ( * inner_size as u64 ) ;
1601+ let col = index % ( * inner_size as u64 ) ;
1602+ format ! ( "{base_label}[{row}][{col}]" )
1603+ } else {
1604+ // 1D array
1605+ format ! ( "{base_label}[{index}]" )
1606+ }
1607+ }
1608+ _ => base_label. to_string ( ) ,
1609+ }
1610+ }
1611+
1612+ /// Helper function to decode a single storage value using its DynSolType
1613+ fn decode_storage_value ( value : B256 , dyn_type : & DynSolType ) -> Option < DynSolValue > {
1614+ // Storage values are always 32 bytes, stored as a single word
1615+ // For arrays, we need to unwrap to the base element type
1616+ let mut actual_type = dyn_type;
1617+ // Unwrap nested arrays to get to the base element type.
1618+ while let DynSolType :: FixedArray ( elem_type, _) = actual_type {
1619+ actual_type = elem_type. as_ref ( ) ;
1620+ }
1621+
1622+ // Use abi_decode to decode the value
1623+ actual_type. abi_decode ( & value. 0 ) . ok ( )
1624+ }
1625+
1626+ /// Helper function to format DynSolValue as raw string without type information
1627+ fn format_dyn_sol_value_raw ( value : & DynSolValue ) -> String {
1628+ match value {
1629+ DynSolValue :: Bool ( b) => b. to_string ( ) ,
1630+ DynSolValue :: Int ( i, _) => i. to_string ( ) ,
1631+ DynSolValue :: Uint ( u, _) => u. to_string ( ) ,
1632+ DynSolValue :: FixedBytes ( bytes, size) => hex:: encode_prefixed ( & bytes. 0 [ ..* size] ) ,
1633+ DynSolValue :: Address ( addr) => addr. to_string ( ) ,
1634+ DynSolValue :: Function ( func) => func. as_address_and_selector ( ) . 1 . to_string ( ) ,
1635+ DynSolValue :: Bytes ( bytes) => hex:: encode_prefixed ( bytes) ,
1636+ DynSolValue :: String ( s) => s. clone ( ) ,
1637+ DynSolValue :: Array ( values) | DynSolValue :: FixedArray ( values) => {
1638+ let formatted: Vec < String > = values. iter ( ) . map ( format_dyn_sol_value_raw) . collect ( ) ;
1639+ format ! ( "[{}]" , formatted. join( ", " ) )
1640+ }
1641+ DynSolValue :: Tuple ( values) => {
1642+ let formatted: Vec < String > = values. iter ( ) . map ( format_dyn_sol_value_raw) . collect ( ) ;
1643+ format ! ( "({})" , formatted. join( ", " ) )
1644+ }
1645+ DynSolValue :: CustomStruct { name : _, prop_names : _, tuple } => {
1646+ format_dyn_sol_value_raw ( & DynSolValue :: Tuple ( tuple. clone ( ) ) )
1647+ }
1648+ }
1649+ }
1650+
13781651/// Helper function to set / unset cold storage slot of the target address.
13791652fn set_cold_slot ( ccx : & mut CheatsCtxt , target : Address , slot : U256 , cold : bool ) {
13801653 if let Some ( account) = ccx. ecx . journaled_state . state . get_mut ( & target)
0 commit comments