@@ -6,11 +6,13 @@ use crate::{
6
6
inspector:: { Ecx , RecordDebugStepInfo } ,
7
7
} ;
8
8
use alloy_consensus:: TxEnvelope ;
9
+ use alloy_dyn_abi:: { DynSolType , DynSolValue } ;
9
10
use alloy_genesis:: { Genesis , GenesisAccount } ;
10
- use alloy_primitives:: { Address , B256 , U256 , map:: HashMap } ;
11
+ use alloy_primitives:: { Address , B256 , U256 , hex , map:: HashMap } ;
11
12
use alloy_rlp:: Decodable ;
12
13
use alloy_sol_types:: SolValue ;
13
14
use foundry_common:: fs:: { read_json_file, write_json_file} ;
15
+ use foundry_compilers:: artifacts:: StorageLayout ;
14
16
use foundry_evm_core:: {
15
17
ContextExt ,
16
18
backend:: { DatabaseExt , RevertStateSnapshotAction } ,
@@ -30,6 +32,7 @@ use std::{
30
32
collections:: { BTreeMap , HashSet , btree_map:: Entry } ,
31
33
fmt:: Display ,
32
34
path:: Path ,
35
+ str:: FromStr ,
33
36
} ;
34
37
35
38
mod record_debug_step;
@@ -103,6 +106,58 @@ struct SlotStateDiff {
103
106
previous_value : B256 ,
104
107
/// Current storage value.
105
108
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
+ }
106
161
}
107
162
108
163
/// Balance diff info.
@@ -170,11 +225,38 @@ impl Display for AccountStateDiffs {
170
225
if !& self . state_diff . is_empty ( ) {
171
226
writeln ! ( f, "- state diff:" ) ?;
172
227
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
+ }
178
260
}
179
261
}
180
262
@@ -1257,12 +1339,20 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap<Address, AccountSt
1257
1339
}
1258
1340
}
1259
1341
1260
- // Look up contract names for all addresses
1342
+ // Look up contract names and storage layouts for all addresses
1261
1343
let mut contract_names = HashMap :: new ( ) ;
1344
+ let mut storage_layouts = HashMap :: new ( ) ;
1262
1345
for address in addresses_to_lookup {
1263
1346
if let Some ( name) = get_contract_name ( ccx, address) {
1264
1347
contract_names. insert ( address, name) ;
1265
1348
}
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
+ }
1266
1356
}
1267
1357
1268
1358
// Now process the records
@@ -1331,13 +1421,44 @@ fn get_recorded_state_diffs(ccx: &mut CheatsCtxt) -> BTreeMap<Address, AccountSt
1331
1421
// Update state diff. Do not overwrite the initial value if already set.
1332
1422
match account_diff. state_diff . entry ( storage_access. slot ) {
1333
1423
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
+
1334
1442
slot_state_diff. insert ( SlotStateDiff {
1335
1443
previous_value : storage_access. previousValue ,
1336
1444
new_value : storage_access. newValue ,
1445
+ decoded,
1446
+ slot_info,
1337
1447
} ) ;
1338
1448
}
1339
1449
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
+ }
1341
1462
}
1342
1463
}
1343
1464
}
@@ -1375,6 +1496,158 @@ fn get_contract_name(ccx: &mut CheatsCtxt, address: Address) -> Option<String> {
1375
1496
None
1376
1497
}
1377
1498
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
+
1378
1651
/// Helper function to set / unset cold storage slot of the target address.
1379
1652
fn set_cold_slot ( ccx : & mut CheatsCtxt , target : Address , slot : U256 , cold : bool ) {
1380
1653
if let Some ( account) = ccx. ecx . journaled_state . state . get_mut ( & target)
0 commit comments