foundry_common/slot_identifier.rs
1//! Storage slot identification and decoding utilities for Solidity storage layouts.
2//!
3//! This module provides functionality to identify and decode storage slots based on
4//! Solidity storage layout information from the compiler.
5
6use crate::mapping_slots::MappingSlots;
7use alloy_dyn_abi::{DynSolType, DynSolValue};
8use alloy_primitives::{B256, U256, hex, keccak256, map::B256Map};
9use foundry_common_fmt::format_token_raw;
10use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType};
11use serde::Serialize;
12use std::{collections::BTreeMap, str::FromStr, sync::Arc};
13use tracing::trace;
14
15/// "inplace" encoding type for variables that fit in one storage slot i.e 32 bytes
16pub const ENCODING_INPLACE: &str = "inplace";
17/// "mapping" encoding type for Solidity mappings, which use keccak256 hash-based storage
18pub const ENCODING_MAPPING: &str = "mapping";
19/// "bytes" encoding type for bytes and string types, which use either inplace or keccak256
20/// hash-based storage depending on length
21pub const ENCODING_BYTES: &str = "bytes";
22/// "dynamic_array" encoding type for dynamic arrays, which uses keccak256 hash-based storage
23pub const ENCODING_DYN_ARRAY: &str = "dynamic_array";
24
25/// Information about a storage slot including its label, type, and decoded values.
26#[derive(Serialize, Debug)]
27pub struct SlotInfo {
28 /// The variable name from the storage layout.
29 ///
30 /// For top-level variables: just the variable name (e.g., "myVariable")
31 /// For struct members: dotted path (e.g., "myStruct.memberName")
32 /// For array elements: name with indices (e.g., "myArray\[0\]", "matrix\[1\]\[2\]")
33 /// For nested structures: full path (e.g., "outer.inner.field")
34 /// For mappings: base name with keys (e.g., "balances\[0x1234...\]")/ex
35 pub label: String,
36 /// The Solidity type information
37 #[serde(rename = "type", serialize_with = "serialize_slot_type")]
38 pub slot_type: StorageTypeInfo,
39 /// Offset within the storage slot (for packed storage)
40 pub offset: i64,
41 /// The storage slot number as a string
42 pub slot: String,
43 /// For struct members, contains nested SlotInfo for each member
44 ///
45 /// This is populated when a struct's members / fields are packed in a single slot.
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub members: Option<Vec<Self>>,
48 /// Decoded values (if available) - used for struct members
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub decoded: Option<DecodedSlotValues>,
51 /// Decoded mapping keys (serialized as "key" for single, "keys" for multiple)
52 #[serde(
53 skip_serializing_if = "Option::is_none",
54 flatten,
55 serialize_with = "serialize_mapping_keys"
56 )]
57 pub keys: Option<Vec<String>>,
58}
59
60/// Wrapper type that holds both the original type label and the parsed DynSolType.
61///
62/// We need both because:
63/// - `label`: Used for serialization to ensure output matches user expectations
64/// - `dyn_sol_type`: The parsed type used for actual value decoding
65#[derive(Debug)]
66pub struct StorageTypeInfo {
67 /// The original type label from storage layout (e.g., "uint256", "address", "mapping(address
68 /// => uint256)")
69 pub label: String,
70 /// The parsed dynamic Solidity type used for decoding
71 pub dyn_sol_type: DynSolType,
72}
73
74impl SlotInfo {
75 /// Decodes a single storage value based on the slot's type information.
76 ///
77 /// Note: For decoding [`DynSolType::Bytes`] or [`DynSolType::String`] that span multiple slots,
78 /// use [`SlotInfo::decode_bytes_or_string`].
79 pub fn decode(&self, value: B256) -> Option<DynSolValue> {
80 // Storage values are always 32 bytes, stored as a single word
81 let mut actual_type = &self.slot_type.dyn_sol_type;
82 // Unwrap nested arrays to get to the base element type.
83 while let DynSolType::FixedArray(elem_type, _) = actual_type {
84 actual_type = elem_type.as_ref();
85 }
86
87 // Special handling for bytes and string types
88 match actual_type {
89 DynSolType::Bytes | DynSolType::String => {
90 // Decode bytes/string from storage
91 // The last byte contains the length * 2 for short strings/bytes
92 // or length * 2 + 1 for long strings/bytes
93 let length_byte = value.0[31];
94
95 if length_byte & 1 == 0 {
96 // Short string/bytes (less than 32 bytes)
97 let length = (length_byte >> 1) as usize;
98 // Extract data
99 let data = if length == 0 { Vec::new() } else { value.0[0..length].to_vec() };
100
101 // Create the appropriate value based on type
102 if matches!(actual_type, DynSolType::String) {
103 let str_val = if data.is_empty() {
104 String::new()
105 } else {
106 String::from_utf8(data).unwrap_or_default()
107 };
108 Some(DynSolValue::String(str_val))
109 } else {
110 Some(DynSolValue::Bytes(data))
111 }
112 } else {
113 // Long string/bytes (32 bytes or more)
114 // The actual data is stored at keccak256(slot)
115 // Return None for long values - they need decode_bytes_or_string()
116 None
117 }
118 }
119 _ => {
120 // Decode based on the actual type
121 actual_type.abi_decode(&value.0).ok()
122 }
123 }
124 }
125
126 /// Slot is of type [`DynSolType::Bytes`] or [`DynSolType::String`]
127 pub fn is_bytes_or_string(&self) -> bool {
128 matches!(self.slot_type.dyn_sol_type, DynSolType::Bytes | DynSolType::String)
129 }
130
131 /// Decodes a [`DynSolType::Bytes`] or [`DynSolType::String`] value
132 /// that spans across multiple slots.
133 pub fn decode_bytes_or_string(
134 &mut self,
135 base_slot: &B256,
136 storage_values: &B256Map<B256>,
137 ) -> Option<DynSolValue> {
138 // Only process bytes/string types
139 if !self.is_bytes_or_string() {
140 return None;
141 }
142
143 // Try to handle as long bytes/string
144 self.aggregate_bytes_or_strings(base_slot, storage_values).map(|data| {
145 match self.slot_type.dyn_sol_type {
146 DynSolType::String => {
147 DynSolValue::String(String::from_utf8(data).unwrap_or_default())
148 }
149 DynSolType::Bytes => DynSolValue::Bytes(data),
150 _ => unreachable!(),
151 }
152 })
153 }
154
155 /// Decodes both previous and new [`DynSolType::Bytes`] or [`DynSolType::String`] values
156 /// that span across multiple slots using state diff data.
157 ///
158 /// Accepts a mapping of storage_slot to (previous_value, new_value).
159 pub fn decode_bytes_or_string_values(
160 &mut self,
161 base_slot: &B256,
162 storage_accesses: &BTreeMap<B256, (B256, B256)>,
163 ) {
164 // Only process bytes/string types
165 if !self.is_bytes_or_string() {
166 return;
167 }
168
169 // Get both previous and new values from the storage accesses
170 if let Some((prev_base_value, new_base_value)) = storage_accesses.get(base_slot) {
171 // Reusable closure to decode bytes/string based on length encoding
172 let mut decode_value = |base_value: B256, is_new: bool| {
173 let length_byte = base_value.0[31];
174 if length_byte & 1 == 1 {
175 // Long bytes/string - aggregate from multiple slots
176 let value_map = storage_accesses
177 .iter()
178 .map(|(slot, (prev, new))| (*slot, if is_new { *new } else { *prev }))
179 .collect::<B256Map<_>>();
180 self.decode_bytes_or_string(base_slot, &value_map)
181 } else {
182 // Short bytes/string - decode directly from base slot
183 self.decode(base_value)
184 }
185 };
186
187 // Decode previous value
188 let prev_decoded = decode_value(*prev_base_value, false);
189
190 // Decode new value
191 let new_decoded = decode_value(*new_base_value, true);
192
193 // Set decoded values if both were successfully decoded
194 if let (Some(prev), Some(new)) = (prev_decoded, new_decoded) {
195 self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
196 }
197 }
198 }
199
200 /// Aggregates a [`DynSolType::Bytes`] or [`DynSolType::String`] value that spans across
201 /// multiple slots by looking up the length in the base_slot.
202 ///
203 /// Returns the aggregated raw bytes.
204 fn aggregate_bytes_or_strings(
205 &mut self,
206 base_slot: &B256,
207 storage_values: &B256Map<B256>,
208 ) -> Option<Vec<u8>> {
209 if !self.is_bytes_or_string() {
210 return None;
211 }
212
213 // Check if it's a long bytes/string by looking at the base value
214 if let Some(base_value) = storage_values.get(base_slot) {
215 let length_byte = base_value.0[31];
216
217 // Check if value is long
218 if length_byte & 1 == 1 {
219 // Long bytes/string - populate members
220 let length: U256 = U256::from_be_bytes(base_value.0) >> 1;
221 let num_slots = length.to::<usize>().div_ceil(32).min(256);
222 let data_start = U256::from_be_bytes(keccak256(base_slot.0).0);
223
224 let mut members = Vec::new();
225 let mut full_data = Vec::with_capacity(length.to::<usize>());
226
227 for i in 0..num_slots {
228 let data_slot = B256::from(data_start + U256::from(i));
229 let data_slot_u256 = data_start + U256::from(i);
230
231 // Create member info for this data slot with indexed label
232 let member_info = Self {
233 label: format!("{}[{}]", self.label, i),
234 slot_type: StorageTypeInfo {
235 label: self.slot_type.label.clone(),
236 dyn_sol_type: DynSolType::FixedBytes(32),
237 },
238 offset: 0,
239 slot: data_slot_u256.to_string(),
240 members: None,
241 decoded: None,
242 keys: None,
243 };
244
245 if let Some(value) = storage_values.get(&data_slot) {
246 // Collect data
247 let bytes_to_take =
248 std::cmp::min(32, length.to::<usize>() - full_data.len());
249 full_data.extend_from_slice(&value.0[..bytes_to_take]);
250 }
251
252 members.push(member_info);
253 }
254
255 // Set the members field
256 if !members.is_empty() {
257 self.members = Some(members);
258 }
259
260 return Some(full_data);
261 }
262 }
263
264 None
265 }
266
267 /// Decodes storage values (previous and new) and populates the decoded field.
268 /// For structs with members, it decodes each member individually.
269 pub fn decode_values(&mut self, previous_value: B256, new_value: B256) {
270 // If this is a struct with members, decode each member individually
271 if let Some(members) = &mut self.members {
272 for member in members.iter_mut() {
273 let offset = member.offset as usize;
274 let size = match &member.slot_type.dyn_sol_type {
275 DynSolType::Uint(bits) | DynSolType::Int(bits) => bits / 8,
276 DynSolType::Address => 20,
277 DynSolType::Bool => 1,
278 DynSolType::FixedBytes(size) => *size,
279 _ => 32, // Default to full word
280 };
281
282 // Extract and decode member values
283 let mut prev_bytes = [0u8; 32];
284 let mut new_bytes = [0u8; 32];
285
286 if offset + size <= 32 {
287 // In Solidity storage, values are right-aligned
288 // For offset 0, we want the rightmost bytes
289 // For offset 16 (for a uint128), we want bytes 0-16
290 // For packed storage: offset 0 is at the rightmost position
291 // offset 0, size 16 -> read bytes 16-32 (rightmost)
292 // offset 16, size 16 -> read bytes 0-16 (leftmost)
293 let byte_start = 32 - offset - size;
294 prev_bytes[32 - size..]
295 .copy_from_slice(&previous_value.0[byte_start..byte_start + size]);
296 new_bytes[32 - size..]
297 .copy_from_slice(&new_value.0[byte_start..byte_start + size]);
298 }
299
300 // Decode the member values
301 if let (Ok(prev_val), Ok(new_val)) = (
302 member.slot_type.dyn_sol_type.abi_decode(&prev_bytes),
303 member.slot_type.dyn_sol_type.abi_decode(&new_bytes),
304 ) {
305 member.decoded =
306 Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val });
307 }
308 }
309 // For structs with members, we don't need a top-level decoded value
310 } else {
311 // For non-struct types, decode directly
312 // Note: decode() returns None for long bytes/strings, which will be handled by
313 // decode_bytes_or_string()
314 if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) {
315 self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
316 }
317 }
318 }
319}
320
321/// Custom serializer for StorageTypeInfo that only outputs the label
322fn serialize_slot_type<S>(info: &StorageTypeInfo, serializer: S) -> Result<S::Ok, S::Error>
323where
324 S: serde::Serializer,
325{
326 serializer.serialize_str(&info.label)
327}
328
329/// Custom serializer for mapping keys
330fn serialize_mapping_keys<S>(keys: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
331where
332 S: serde::Serializer,
333{
334 use serde::ser::SerializeMap;
335
336 if let Some(keys) = keys {
337 let len = if keys.is_empty() { 0 } else { 1 };
338 let mut map = serializer.serialize_map(Some(len))?;
339 if keys.len() == 1 {
340 map.serialize_entry("key", &keys[0])?;
341 } else if keys.len() > 1 {
342 map.serialize_entry("keys", keys)?;
343 }
344 map.end()
345 } else {
346 serializer.serialize_none()
347 }
348}
349
350/// Decoded storage slot values
351#[derive(Debug)]
352pub struct DecodedSlotValues {
353 /// Initial decoded storage value
354 pub previous_value: DynSolValue,
355 /// Current decoded storage value
356 pub new_value: DynSolValue,
357}
358
359impl Serialize for DecodedSlotValues {
360 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
361 where
362 S: serde::Serializer,
363 {
364 use serde::ser::SerializeStruct;
365
366 let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?;
367 state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?;
368 state.serialize_field("newValue", &format_token_raw(&self.new_value))?;
369 state.end()
370 }
371}
372
373/// Storage slot identifier that uses Solidity [`StorageLayout`] to identify storage slots.
374pub struct SlotIdentifier {
375 storage_layout: Arc<StorageLayout>,
376}
377
378impl SlotIdentifier {
379 /// Creates a new SlotIdentifier with the given storage layout.
380 pub fn new(storage_layout: Arc<StorageLayout>) -> Self {
381 Self { storage_layout }
382 }
383
384 /// Identifies a storage slots type using the [`StorageLayout`].
385 ///
386 /// It can also identify whether a slot belongs to a mapping if provided with [`MappingSlots`].
387 pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
388 trace!(?slot, "identifying slot");
389 let slot_u256 = U256::from_be_bytes(slot.0);
390 let slot_str = slot_u256.to_string();
391
392 for storage in &self.storage_layout.storage {
393 let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
394 let dyn_type = DynSolType::parse(&storage_type.label).ok();
395
396 // Check if we're able to match on a slot from the layout i.e any of the base slots.
397 // This will always be the case for primitive types that fit in a single slot.
398 if storage.slot == slot_str
399 && let Some(parsed_type) = dyn_type
400 {
401 // Successfully parsed - handle arrays or simple types
402 let label = if let DynSolType::FixedArray(_, _) = &parsed_type {
403 format!("{}{}", storage.label, get_array_base_indices(&parsed_type))
404 } else {
405 storage.label.clone()
406 };
407
408 return Some(SlotInfo {
409 label,
410 slot_type: StorageTypeInfo {
411 label: storage_type.label.clone(),
412 dyn_sol_type: parsed_type,
413 },
414 offset: storage.offset,
415 slot: storage.slot.clone(),
416 members: None,
417 decoded: None,
418 keys: None,
419 });
420 }
421
422 // Encoding types: <https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#json-output>
423 if storage_type.encoding == ENCODING_INPLACE {
424 // Can be of type FixedArrays or Structs
425 // Handles the case where the accessed `slot` is maybe different from the base slot.
426 let array_start_slot = U256::from_str(&storage.slot).ok()?;
427
428 if let Some(parsed_type) = dyn_type
429 && let DynSolType::FixedArray(_, _) = parsed_type
430 && let Some(slot_info) = self.handle_array_slot(
431 storage,
432 storage_type,
433 slot_u256,
434 array_start_slot,
435 &slot_str,
436 )
437 {
438 return Some(slot_info);
439 }
440
441 // If type parsing fails and the label is a struct
442 if is_struct(&storage_type.label) {
443 let struct_start_slot = U256::from_str(&storage.slot).ok()?;
444 if let Some(slot_info) = self.handle_struct(
445 &storage.label,
446 storage_type,
447 slot_u256,
448 struct_start_slot,
449 storage.offset,
450 &slot_str,
451 0,
452 ) {
453 return Some(slot_info);
454 }
455 }
456 } else if storage_type.encoding == ENCODING_MAPPING
457 && let Some(mapping_slots) = mapping_slots
458 && let Some(info) =
459 self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots)
460 {
461 return Some(info);
462 }
463 }
464
465 None
466 }
467
468 /// Identifies a bytes or string storage slot by checking all bytes/string variables
469 /// in the storage layout and using their base slot values from the provided storage changes.
470 ///
471 /// # Arguments
472 /// * `slot` - The slot being identified
473 /// * `storage_values` - Map of storage slots to their current values
474 pub fn identify_bytes_or_string(
475 &self,
476 slot: &B256,
477 storage_values: &B256Map<B256>,
478 ) -> Option<SlotInfo> {
479 let slot_u256 = U256::from_be_bytes(slot.0);
480 let slot_str = slot_u256.to_string();
481
482 // Search through all bytes/string variables in the storage layout
483 for storage in &self.storage_layout.storage {
484 if let Some(storage_type) = self.storage_layout.types.get(&storage.storage_type)
485 && storage_type.encoding == ENCODING_BYTES
486 {
487 let Some(base_slot) = U256::from_str(&storage.slot).map(B256::from).ok() else {
488 continue;
489 };
490 // Get the base slot value from storage_values
491 if let Some(base_value) = storage_values.get(&base_slot)
492 && let Some(info) = self.handle_bytes_string(
493 storage,
494 storage_type,
495 slot_u256,
496 &slot_str,
497 base_value,
498 )
499 {
500 return Some(info);
501 }
502 }
503 }
504
505 None
506 }
507
508 /// Handles identification of array slots.
509 ///
510 /// # Arguments
511 /// * `storage` - The storage metadata from the layout
512 /// * `storage_type` - Type information for the storage slot
513 /// * `slot` - The target slot being identified
514 /// * `array_start_slot` - The starting slot of the array in storage i.e base_slot
515 /// * `slot_str` - String representation of the slot for output
516 fn handle_array_slot(
517 &self,
518 storage: &Storage,
519 storage_type: &StorageType,
520 slot: U256,
521 array_start_slot: U256,
522 slot_str: &str,
523 ) -> Option<SlotInfo> {
524 // Check if slot is within array bounds
525 let total_bytes = storage_type.number_of_bytes.parse::<u64>().ok()?;
526 let total_slots = total_bytes.div_ceil(32);
527
528 if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) {
529 let parsed_type = DynSolType::parse(&storage_type.label).ok()?;
530 let index = (slot - array_start_slot).to::<u64>();
531 // Format the array element label based on array dimensions
532 let label = match &parsed_type {
533 DynSolType::FixedArray(inner, _) => {
534 if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() {
535 // 2D array: calculate row and column
536 let row = index / (*inner_size as u64);
537 let col = index % (*inner_size as u64);
538 format!("{}[{row}][{col}]", storage.label)
539 } else {
540 // 1D array
541 format!("{}[{index}]", storage.label)
542 }
543 }
544 _ => storage.label.clone(),
545 };
546
547 return Some(SlotInfo {
548 label,
549 slot_type: StorageTypeInfo {
550 label: storage_type.label.clone(),
551 dyn_sol_type: parsed_type,
552 },
553 offset: 0,
554 slot: slot_str.to_string(),
555 members: None,
556 decoded: None,
557 keys: None,
558 });
559 }
560
561 None
562 }
563
564 /// Handles identification of struct slots.
565 ///
566 /// Recursively resolves struct members to find the exact member corresponding
567 /// to the target slot. Handles both single-slot (packed) and multi-slot structs.
568 ///
569 /// # Arguments
570 /// * `base_label` - The label/name for this struct or member
571 /// * `storage_type` - Type information for the storage
572 /// * `target_slot` - The target slot being identified
573 /// * `struct_start_slot` - The starting slot of this struct
574 /// * `offset` - Offset within the slot (for packed storage)
575 /// * `slot_str` - String representation of the slot for output
576 /// * `depth` - Current recursion depth
577 #[allow(clippy::too_many_arguments)]
578 fn handle_struct(
579 &self,
580 base_label: &str,
581 storage_type: &StorageType,
582 target_slot: U256,
583 struct_start_slot: U256,
584 offset: i64,
585 slot_str: &str,
586 depth: usize,
587 ) -> Option<SlotInfo> {
588 // Limit recursion depth to prevent stack overflow
589 const MAX_DEPTH: usize = 10;
590 if depth > MAX_DEPTH {
591 return None;
592 }
593
594 let members = storage_type
595 .other
596 .get("members")
597 .and_then(|v| serde_json::from_value::<Vec<Storage>>(v.clone()).ok())?;
598
599 // If this is the exact slot we're looking for (struct's base slot)
600 if struct_start_slot == target_slot
601 // Find the member at slot offset 0 (the member that starts at this slot)
602 && let Some(first_member) = members.iter().find(|m| m.slot == "0")
603 {
604 let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?;
605
606 // Check if we have a single-slot struct (all members have slot "0")
607 let is_single_slot = members.iter().all(|m| m.slot == "0");
608
609 if is_single_slot {
610 // Build member info for single-slot struct
611 let mut member_infos = Vec::new();
612 for member in &members {
613 if let Some(member_type_info) =
614 self.storage_layout.types.get(&member.storage_type)
615 && let Some(member_type) = DynSolType::parse(&member_type_info.label).ok()
616 {
617 member_infos.push(SlotInfo {
618 label: member.label.clone(),
619 slot_type: StorageTypeInfo {
620 label: member_type_info.label.clone(),
621 dyn_sol_type: member_type,
622 },
623 offset: member.offset,
624 slot: slot_str.to_string(),
625 members: None,
626 decoded: None,
627 keys: None,
628 });
629 }
630 }
631
632 // Build the CustomStruct type
633 let struct_name =
634 storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label);
635 let prop_names: Vec<String> = members.iter().map(|m| m.label.clone()).collect();
636 let member_types: Vec<DynSolType> =
637 member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect();
638
639 let parsed_type = DynSolType::CustomStruct {
640 name: struct_name.to_string(),
641 prop_names,
642 tuple: member_types,
643 };
644
645 return Some(SlotInfo {
646 label: base_label.to_string(),
647 slot_type: StorageTypeInfo {
648 label: storage_type.label.clone(),
649 dyn_sol_type: parsed_type,
650 },
651 offset,
652 slot: slot_str.to_string(),
653 decoded: None,
654 members: if member_infos.is_empty() { None } else { Some(member_infos) },
655 keys: None,
656 });
657 } else {
658 // Multi-slot struct - return the first member.
659 let member_label = format!("{}.{}", base_label, first_member.label);
660
661 // If the first member is itself a struct, recurse
662 if is_struct(&member_type_info.label) {
663 return self.handle_struct(
664 &member_label,
665 member_type_info,
666 target_slot,
667 struct_start_slot,
668 first_member.offset,
669 slot_str,
670 depth + 1,
671 );
672 }
673
674 // Return the first member as a primitive
675 return Some(SlotInfo {
676 label: member_label,
677 slot_type: StorageTypeInfo {
678 label: member_type_info.label.clone(),
679 dyn_sol_type: DynSolType::parse(&member_type_info.label).ok()?,
680 },
681 offset: first_member.offset,
682 slot: slot_str.to_string(),
683 decoded: None,
684 members: None,
685 keys: None,
686 });
687 }
688 }
689
690 // Not the base slot - search through members
691 for member in &members {
692 let member_slot_offset = U256::from_str(&member.slot).ok()?;
693 let member_slot = struct_start_slot + member_slot_offset;
694 let member_type_info = self.storage_layout.types.get(&member.storage_type)?;
695 let member_label = format!("{}.{}", base_label, member.label);
696
697 // If this member is a struct, recurse into it
698 if is_struct(&member_type_info.label) {
699 let slot_info = self.handle_struct(
700 &member_label,
701 member_type_info,
702 target_slot,
703 member_slot,
704 member.offset,
705 slot_str,
706 depth + 1,
707 );
708
709 if member_slot == target_slot || slot_info.is_some() {
710 return slot_info;
711 }
712 }
713
714 if member_slot == target_slot {
715 // Found the exact member slot
716
717 // Regular member
718 let member_type = DynSolType::parse(&member_type_info.label).ok()?;
719 return Some(SlotInfo {
720 label: member_label,
721 slot_type: StorageTypeInfo {
722 label: member_type_info.label.clone(),
723 dyn_sol_type: member_type,
724 },
725 offset: member.offset,
726 slot: slot_str.to_string(),
727 members: None,
728 decoded: None,
729 keys: None,
730 });
731 }
732 }
733
734 None
735 }
736
737 /// Handles identification of mapping slots.
738 ///
739 /// Identifies mapping entries by walking up the parent chain to find the base slot,
740 /// then decodes the keys and builds the appropriate label.
741 ///
742 /// # Arguments
743 /// * `storage` - The storage metadata from the layout
744 /// * `storage_type` - Type information for the storage
745 /// * `slot` - The accessed slot being identified
746 /// * `slot_str` - String representation of the slot for output
747 /// * `mapping_slots` - Tracked mapping slot accesses for key resolution
748 fn handle_mapping(
749 &self,
750 storage: &Storage,
751 storage_type: &StorageType,
752 slot: &B256,
753 slot_str: &str,
754 mapping_slots: &MappingSlots,
755 ) -> Option<SlotInfo> {
756 trace!(
757 "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}",
758 storage.slot,
759 slot,
760 mapping_slots.keys.contains_key(slot),
761 mapping_slots.parent_slots.contains_key(slot)
762 );
763
764 // Verify it's actually a mapping type
765 if storage_type.encoding != ENCODING_MAPPING {
766 return None;
767 }
768
769 // Check if this slot is a known mapping entry
770 if !mapping_slots.keys.contains_key(slot) {
771 return None;
772 }
773
774 // Convert storage.slot to B256 for comparison
775 let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?);
776
777 // Walk up the parent chain to collect keys and validate the base slot
778 let mut current_slot = *slot;
779 let mut keys_to_decode = Vec::new();
780 let mut found_base = false;
781
782 while let Some((key, parent)) =
783 mapping_slots.keys.get(¤t_slot).zip(mapping_slots.parent_slots.get(¤t_slot))
784 {
785 keys_to_decode.push(*key);
786
787 // Check if the parent is our base storage slot
788 if *parent == storage_slot_b256 {
789 found_base = true;
790 break;
791 }
792
793 // Move up to the parent for the next iteration
794 current_slot = *parent;
795 }
796
797 if !found_base {
798 trace!("Mapping slot {} does not match any parent in chain", storage.slot);
799 return None;
800 }
801
802 // Resolve the mapping type to get all key types and the final value type
803 let (key_types, value_type_label, full_type_label) =
804 self.resolve_mapping_type(&storage.storage_type)?;
805
806 // Reverse keys to process from outermost to innermost
807 keys_to_decode.reverse();
808
809 // Build the label with decoded keys and collect decoded key values
810 let mut label = storage.label.clone();
811 let mut decoded_keys = Vec::new();
812
813 // Decode each key using the corresponding type
814 for (i, key) in keys_to_decode.iter().enumerate() {
815 if let Some(key_type_label) = key_types.get(i)
816 && let Ok(sol_type) = DynSolType::parse(key_type_label)
817 && let Ok(decoded) = sol_type.abi_decode(&key.0)
818 {
819 let decoded_key_str = format_token_raw(&decoded);
820 decoded_keys.push(decoded_key_str.clone());
821 label = format!("{label}[{decoded_key_str}]");
822 } else {
823 let hex_key = hex::encode_prefixed(key.0);
824 decoded_keys.push(hex_key.clone());
825 label = format!("{label}[{hex_key}]");
826 }
827 }
828
829 // Parse the final value type for decoding
830 let dyn_sol_type = DynSolType::parse(&value_type_label).unwrap_or(DynSolType::Bytes);
831
832 Some(SlotInfo {
833 label,
834 slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type },
835 offset: storage.offset,
836 slot: slot_str.to_string(),
837 members: None,
838 decoded: None,
839 keys: Some(decoded_keys),
840 })
841 }
842
843 /// Handles identification of bytes/string storage slots.
844 ///
845 /// Bytes and strings in Solidity use a special storage layout:
846 /// - Short values (<32 bytes): stored in the same slot with length * 2
847 /// - Long values (>=32 bytes): length * 2 + 1 in main slot, data at keccak256(slot)
848 ///
849 /// This function checks if the given slot is:
850 /// 1. A main slot for a bytes/string variable
851 /// 2. A data slot for any long bytes/string variable in the storage layout
852 ///
853 /// # Arguments
854 /// * `slot` - The accessed slot being identified
855 /// * `slot_str` - String representation of the slot for output
856 /// * `base_slot_value` - The value at the base slot (used to determine length for long
857 /// bytes/strings)
858 fn handle_bytes_string(
859 &self,
860 storage: &Storage,
861 storage_type: &StorageType,
862 slot: U256,
863 slot_str: &str,
864 base_slot_value: &B256,
865 ) -> Option<SlotInfo> {
866 // Only handle bytes/string encoded variables for this specific storage entry
867 if storage_type.encoding != ENCODING_BYTES {
868 return None;
869 }
870
871 // Check if this is the main slot for this variable
872 let base_slot = U256::from_str(&storage.slot).ok()?;
873 if slot == base_slot {
874 // Parse the type to get the correct DynSolType
875 let dyn_type = if storage_type.label == "string" {
876 DynSolType::String
877 } else if storage_type.label == "bytes" {
878 DynSolType::Bytes
879 } else {
880 return None;
881 };
882
883 return Some(SlotInfo {
884 label: storage.label.clone(),
885 slot_type: StorageTypeInfo {
886 label: storage_type.label.clone(),
887 dyn_sol_type: dyn_type,
888 },
889 offset: storage.offset,
890 slot: slot_str.to_string(),
891 members: None,
892 decoded: None,
893 keys: None,
894 });
895 }
896
897 // Check if it could be a data slot for this long bytes/string
898 // Calculate where data slots would start for this variable
899 let data_start =
900 U256::from_be_bytes(alloy_primitives::keccak256(base_slot.to_be_bytes::<32>()).0);
901
902 // Get the length from the base slot value to calculate exact number of slots
903 // For long bytes/strings, the length is stored as (length * 2 + 1) in the base slot
904 let length_byte = base_slot_value.0[31];
905 if length_byte & 1 == 1 {
906 // It's a long bytes/string
907 let length = U256::from_be_bytes(base_slot_value.0) >> 1;
908 // Calculate number of slots needed (round up)
909 let num_slots = (length + U256::from(31)) / U256::from(32);
910
911 // Check if our slot is within the data region
912 if slot >= data_start && slot < data_start + num_slots {
913 let slot_index = (slot - data_start).to::<usize>();
914
915 return Some(SlotInfo {
916 label: format!("{}[{}]", storage.label, slot_index),
917 slot_type: StorageTypeInfo {
918 label: storage_type.label.clone(),
919 // Type is assigned as FixedBytes(32) for data slots
920 dyn_sol_type: DynSolType::FixedBytes(32),
921 },
922 offset: 0,
923 slot: slot_str.to_string(),
924 members: None,
925 decoded: None,
926 keys: None,
927 });
928 }
929 }
930
931 None
932 }
933
934 fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec<String>, String, String)> {
935 let storage_type = self.storage_layout.types.get(type_ref)?;
936
937 if storage_type.encoding != ENCODING_MAPPING {
938 // Not a mapping, return the type as-is
939 return Some((vec![], storage_type.label.clone(), storage_type.label.clone()));
940 }
941
942 // Get key and value type references
943 let key_type_ref = storage_type.key.as_ref()?;
944 let value_type_ref = storage_type.value.as_ref()?;
945
946 // Resolve the key type
947 let key_type = self.storage_layout.types.get(key_type_ref)?;
948 let mut key_types = vec![key_type.label.clone()];
949
950 // Check if the value is another mapping (nested case)
951 if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
952 if value_storage_type.encoding == ENCODING_MAPPING {
953 // Recursively resolve the nested mapping
954 let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
955 key_types.extend(nested_keys);
956 return Some((key_types, final_value, storage_type.label.clone()));
957 } else {
958 // Value is not a mapping, we're done
959 return Some((
960 key_types,
961 value_storage_type.label.clone(),
962 storage_type.label.clone(),
963 ));
964 }
965 }
966
967 None
968 }
969}
970
971/// Returns the base indices for array types, e.g. "\[0\]\[0\]" for 2D arrays.
972fn get_array_base_indices(dyn_type: &DynSolType) -> String {
973 match dyn_type {
974 DynSolType::FixedArray(inner, _) => {
975 if let DynSolType::FixedArray(_, _) = inner.as_ref() {
976 // Nested array (2D or higher)
977 format!("[0]{}", get_array_base_indices(inner))
978 } else {
979 // Simple 1D array
980 "[0]".to_string()
981 }
982 }
983 _ => String::new(),
984 }
985}
986
987/// Checks if a given type label represents a struct type.
988pub fn is_struct(s: &str) -> bool {
989 s.starts_with("struct ")
990}