Skip to main content

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(Clone, 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(Clone, 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).min(31);
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 const 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 byte_len = length.try_into().unwrap_or(usize::MAX);
222                let num_slots = byte_len.div_ceil(32).min(256);
223                let data_start = U256::from_be_bytes(keccak256(base_slot.0).0);
224
225                let mut members = Vec::new();
226                let mut full_data = Vec::with_capacity(num_slots * 32);
227
228                for i in 0..num_slots {
229                    let data_slot = B256::from(data_start + U256::from(i));
230                    let data_slot_u256 = data_start + U256::from(i);
231
232                    // Create member info for this data slot with indexed label
233                    let member_info = Self {
234                        label: format!("{}[{}]", self.label, i),
235                        slot_type: StorageTypeInfo {
236                            label: self.slot_type.label.clone(),
237                            dyn_sol_type: DynSolType::FixedBytes(32),
238                        },
239                        offset: 0,
240                        slot: data_slot_u256.to_string(),
241                        members: None,
242                        decoded: None,
243                        keys: None,
244                    };
245
246                    if let Some(value) = storage_values.get(&data_slot) {
247                        // Collect data
248                        let bytes_to_take =
249                            std::cmp::min(32, byte_len.saturating_sub(full_data.len()));
250                        full_data.extend_from_slice(&value.0[..bytes_to_take]);
251                    }
252
253                    members.push(member_info);
254                }
255
256                // Set the members field
257                if !members.is_empty() {
258                    self.members = Some(members);
259                }
260
261                return Some(full_data);
262            }
263        }
264
265        None
266    }
267
268    /// Decodes storage values (previous and new) and populates the decoded field.
269    /// For structs with members, it decodes each member individually.
270    pub fn decode_values(&mut self, previous_value: B256, new_value: B256) {
271        // If this is a struct with members, decode each member individually
272        if let Some(members) = &mut self.members {
273            for member in members.iter_mut() {
274                let offset = member.offset as usize;
275                let size = match &member.slot_type.dyn_sol_type {
276                    DynSolType::Uint(bits) | DynSolType::Int(bits) => bits / 8,
277                    DynSolType::Address => 20,
278                    DynSolType::Bool => 1,
279                    DynSolType::FixedBytes(size) => *size,
280                    _ => 32, // Default to full word
281                };
282
283                // Extract and decode member values
284                let mut prev_bytes = [0u8; 32];
285                let mut new_bytes = [0u8; 32];
286
287                if offset + size <= 32 {
288                    // In Solidity storage, values are right-aligned
289                    // For offset 0, we want the rightmost bytes
290                    // For offset 16 (for a uint128), we want bytes 0-16
291                    // For packed storage: offset 0 is at the rightmost position
292                    // offset 0, size 16 -> read bytes 16-32 (rightmost)
293                    // offset 16, size 16 -> read bytes 0-16 (leftmost)
294                    let byte_start = 32 - offset - size;
295                    prev_bytes[32 - size..]
296                        .copy_from_slice(&previous_value.0[byte_start..byte_start + size]);
297                    new_bytes[32 - size..]
298                        .copy_from_slice(&new_value.0[byte_start..byte_start + size]);
299                }
300
301                // Decode the member values
302                if let (Ok(prev_val), Ok(new_val)) = (
303                    member.slot_type.dyn_sol_type.abi_decode(&prev_bytes),
304                    member.slot_type.dyn_sol_type.abi_decode(&new_bytes),
305                ) {
306                    member.decoded =
307                        Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val });
308                }
309            }
310            // For structs with members, we don't need a top-level decoded value
311        } else {
312            // For non-struct types, decode directly
313            // Note: decode() returns None for long bytes/strings, which will be handled by
314            // decode_bytes_or_string()
315            if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) {
316                self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
317            }
318        }
319    }
320}
321
322/// Custom serializer for StorageTypeInfo that only outputs the label
323fn serialize_slot_type<S>(info: &StorageTypeInfo, serializer: S) -> Result<S::Ok, S::Error>
324where
325    S: serde::Serializer,
326{
327    serializer.serialize_str(&info.label)
328}
329
330/// Custom serializer for mapping keys
331fn serialize_mapping_keys<S>(keys: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
332where
333    S: serde::Serializer,
334{
335    use serde::ser::SerializeMap;
336
337    if let Some(keys) = keys {
338        let len = if keys.is_empty() { 0 } else { 1 };
339        let mut map = serializer.serialize_map(Some(len))?;
340        if keys.len() == 1 {
341            map.serialize_entry("key", &keys[0])?;
342        } else if keys.len() > 1 {
343            map.serialize_entry("keys", keys)?;
344        }
345        map.end()
346    } else {
347        serializer.serialize_none()
348    }
349}
350
351/// Decoded storage slot values
352#[derive(Clone, Debug)]
353pub struct DecodedSlotValues {
354    /// Initial decoded storage value
355    pub previous_value: DynSolValue,
356    /// Current decoded storage value
357    pub new_value: DynSolValue,
358}
359
360impl Serialize for DecodedSlotValues {
361    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
362    where
363        S: serde::Serializer,
364    {
365        use serde::ser::SerializeStruct;
366
367        let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?;
368        state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?;
369        state.serialize_field("newValue", &format_token_raw(&self.new_value))?;
370        state.end()
371    }
372}
373
374/// Storage slot identifier that uses Solidity [`StorageLayout`] to identify storage slots.
375#[derive(Clone)]
376pub struct SlotIdentifier {
377    storage_layout: Arc<StorageLayout>,
378    parsed_types: BTreeMap<String, Option<DynSolType>>,
379}
380
381impl SlotIdentifier {
382    /// Creates a new SlotIdentifier with the given storage layout.
383    pub fn new(storage_layout: Arc<StorageLayout>) -> Self {
384        let parsed_types = storage_layout
385            .types
386            .iter()
387            .map(|(id, storage_type)| (id.clone(), parse_sol_type(&storage_type.label)))
388            .collect();
389        Self { storage_layout, parsed_types }
390    }
391
392    fn parsed_type(&self, storage_type: &str) -> Option<&DynSolType> {
393        self.parsed_types.get(storage_type).and_then(Option::as_ref)
394    }
395
396    /// Identifies a storage slots type using the [`StorageLayout`].
397    ///
398    /// It can also identify whether a slot belongs to a mapping if provided with [`MappingSlots`].
399    pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
400        trace!(?slot, "identifying slot");
401        let slot_u256 = U256::from_be_bytes(slot.0);
402        let slot_str = slot_u256.to_string();
403
404        for storage in &self.storage_layout.storage {
405            let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
406            let dyn_type = self.parsed_type(&storage.storage_type);
407
408            // Check if we're able to match on a slot from the layout i.e any of the base slots.
409            // This will always be the case for primitive types that fit in a single slot.
410            if storage.slot == slot_str
411                && let Some(parsed_type) = dyn_type.cloned()
412            {
413                // Successfully parsed - handle arrays or simple types
414                let label = if let DynSolType::FixedArray(_, _) = &parsed_type {
415                    format!("{}{}", storage.label, get_array_base_indices(&parsed_type))
416                } else {
417                    storage.label.clone()
418                };
419
420                return Some(SlotInfo {
421                    label,
422                    slot_type: StorageTypeInfo {
423                        label: storage_type.label.clone(),
424                        dyn_sol_type: parsed_type,
425                    },
426                    offset: storage.offset,
427                    slot: storage.slot.clone(),
428                    members: None,
429                    decoded: None,
430                    keys: None,
431                });
432            }
433
434            // Encoding types: <https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#json-output>
435            if storage_type.encoding == ENCODING_INPLACE {
436                // Can be of type FixedArrays or Structs
437                // Handles the case where the accessed `slot` is maybe different from the base slot.
438                let array_start_slot = U256::from_str(&storage.slot).ok()?;
439
440                if let Some(parsed_type) = dyn_type
441                    && let DynSolType::FixedArray(_, _) = parsed_type
442                    && let Some(slot_info) = self.handle_array_slot(
443                        storage,
444                        storage_type,
445                        parsed_type,
446                        slot_u256,
447                        array_start_slot,
448                        &slot_str,
449                    )
450                {
451                    return Some(slot_info);
452                }
453
454                // If type parsing fails and the label is a struct
455                if is_struct(&storage_type.label) {
456                    let struct_start_slot = U256::from_str(&storage.slot).ok()?;
457                    if let Some(slot_info) = self.handle_struct(
458                        &storage.label,
459                        storage_type,
460                        slot_u256,
461                        struct_start_slot,
462                        storage.offset,
463                        &slot_str,
464                        0,
465                    ) {
466                        return Some(slot_info);
467                    }
468                }
469            } else if storage_type.encoding == ENCODING_MAPPING
470                && let Some(mapping_slots) = mapping_slots
471                && let Some(info) =
472                    self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots)
473            {
474                return Some(info);
475            }
476        }
477
478        None
479    }
480
481    /// Identifies a bytes or string storage slot by checking all bytes/string variables
482    /// in the storage layout and using their base slot values from the provided storage changes.
483    ///
484    /// # Arguments
485    /// * `slot` - The slot being identified
486    /// * `storage_values` - Map of storage slots to their current values
487    pub fn identify_bytes_or_string(
488        &self,
489        slot: &B256,
490        storage_values: &B256Map<B256>,
491    ) -> Option<SlotInfo> {
492        let slot_u256 = U256::from_be_bytes(slot.0);
493        let slot_str = slot_u256.to_string();
494
495        // Search through all bytes/string variables in the storage layout
496        for storage in &self.storage_layout.storage {
497            if let Some(storage_type) = self.storage_layout.types.get(&storage.storage_type)
498                && storage_type.encoding == ENCODING_BYTES
499            {
500                let Some(base_slot) = U256::from_str(&storage.slot).map(B256::from).ok() else {
501                    continue;
502                };
503                // Get the base slot value from storage_values
504                if let Some(base_value) = storage_values.get(&base_slot)
505                    && let Some(info) = self.handle_bytes_string(
506                        storage,
507                        storage_type,
508                        slot_u256,
509                        &slot_str,
510                        base_value,
511                    )
512                {
513                    return Some(info);
514                }
515            }
516        }
517
518        None
519    }
520
521    /// Handles identification of array slots.
522    ///
523    /// # Arguments
524    /// * `storage` - The storage metadata from the layout
525    /// * `storage_type` - Type information for the storage slot
526    /// * `slot` - The target slot being identified
527    /// * `array_start_slot` - The starting slot of the array in storage i.e base_slot
528    /// * `slot_str` - String representation of the slot for output
529    fn handle_array_slot(
530        &self,
531        storage: &Storage,
532        storage_type: &StorageType,
533        parsed_type: &DynSolType,
534        slot: U256,
535        array_start_slot: U256,
536        slot_str: &str,
537    ) -> Option<SlotInfo> {
538        // Check if slot is within array bounds
539        let total_bytes = storage_type.number_of_bytes.parse::<u64>().ok()?;
540        let total_slots = total_bytes.div_ceil(32);
541
542        if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) {
543            let index = (slot - array_start_slot).to::<u64>();
544            // Format the array element label based on array dimensions
545            let label = match parsed_type {
546                DynSolType::FixedArray(inner, _) => {
547                    if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() {
548                        // 2D array: calculate row and column
549                        let row = index / (*inner_size as u64);
550                        let col = index % (*inner_size as u64);
551                        format!("{}[{row}][{col}]", storage.label)
552                    } else {
553                        // 1D array
554                        format!("{}[{index}]", storage.label)
555                    }
556                }
557                _ => storage.label.clone(),
558            };
559
560            return Some(SlotInfo {
561                label,
562                slot_type: StorageTypeInfo {
563                    label: storage_type.label.clone(),
564                    dyn_sol_type: parsed_type.clone(),
565                },
566                offset: 0,
567                slot: slot_str.to_string(),
568                members: None,
569                decoded: None,
570                keys: None,
571            });
572        }
573
574        None
575    }
576
577    /// Handles identification of struct slots.
578    ///
579    /// Recursively resolves struct members to find the exact member corresponding
580    /// to the target slot. Handles both single-slot (packed) and multi-slot structs.
581    ///
582    /// # Arguments
583    /// * `base_label` - The label/name for this struct or member
584    /// * `storage_type` - Type information for the storage
585    /// * `target_slot` - The target slot being identified
586    /// * `struct_start_slot` - The starting slot of this struct
587    /// * `offset` - Offset within the slot (for packed storage)
588    /// * `slot_str` - String representation of the slot for output
589    /// * `depth` - Current recursion depth
590    #[allow(clippy::too_many_arguments)]
591    fn handle_struct(
592        &self,
593        base_label: &str,
594        storage_type: &StorageType,
595        target_slot: U256,
596        struct_start_slot: U256,
597        offset: i64,
598        slot_str: &str,
599        depth: usize,
600    ) -> Option<SlotInfo> {
601        // Limit recursion depth to prevent stack overflow
602        const MAX_DEPTH: usize = 10;
603        if depth > MAX_DEPTH {
604            return None;
605        }
606
607        let members = storage_type
608            .other
609            .get("members")
610            .and_then(|v| serde_json::from_value::<Vec<Storage>>(v.clone()).ok())?;
611
612        // If this is the exact slot we're looking for (struct's base slot)
613        if struct_start_slot == target_slot
614        // Find the member at slot offset 0 (the member that starts at this slot)
615            && let Some(first_member) = members.iter().find(|m| m.slot == "0")
616        {
617            let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?;
618
619            // Check if we have a single-slot struct (all members have slot "0")
620            let is_single_slot = members.iter().all(|m| m.slot == "0");
621
622            if is_single_slot {
623                // Build member info for single-slot struct
624                let mut member_infos = Vec::new();
625                for member in &members {
626                    if let Some(member_type_info) =
627                        self.storage_layout.types.get(&member.storage_type)
628                        && let Some(member_type) = self.parsed_type(&member.storage_type).cloned()
629                    {
630                        member_infos.push(SlotInfo {
631                            label: member.label.clone(),
632                            slot_type: StorageTypeInfo {
633                                label: member_type_info.label.clone(),
634                                dyn_sol_type: member_type,
635                            },
636                            offset: member.offset,
637                            slot: slot_str.to_string(),
638                            members: None,
639                            decoded: None,
640                            keys: None,
641                        });
642                    }
643                }
644
645                // Build the CustomStruct type
646                let struct_name =
647                    storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label);
648                let prop_names: Vec<String> = members.iter().map(|m| m.label.clone()).collect();
649                let member_types: Vec<DynSolType> =
650                    member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect();
651
652                let parsed_type = DynSolType::CustomStruct {
653                    name: struct_name.to_string(),
654                    prop_names,
655                    tuple: member_types,
656                };
657
658                return Some(SlotInfo {
659                    label: base_label.to_string(),
660                    slot_type: StorageTypeInfo {
661                        label: storage_type.label.clone(),
662                        dyn_sol_type: parsed_type,
663                    },
664                    offset,
665                    slot: slot_str.to_string(),
666                    decoded: None,
667                    members: if member_infos.is_empty() { None } else { Some(member_infos) },
668                    keys: None,
669                });
670            }
671
672            // Multi-slot struct - return the first member.
673            let member_label = format!("{}.{}", base_label, first_member.label);
674
675            // If the first member is itself a struct, recurse
676            if is_struct(&member_type_info.label) {
677                return self.handle_struct(
678                    &member_label,
679                    member_type_info,
680                    target_slot,
681                    struct_start_slot,
682                    first_member.offset,
683                    slot_str,
684                    depth + 1,
685                );
686            }
687
688            // Return the first member as a primitive
689            return Some(SlotInfo {
690                label: member_label,
691                slot_type: StorageTypeInfo {
692                    label: member_type_info.label.clone(),
693                    dyn_sol_type: self.parsed_type(&first_member.storage_type)?.clone(),
694                },
695                offset: first_member.offset,
696                slot: slot_str.to_string(),
697                decoded: None,
698                members: None,
699                keys: None,
700            });
701        }
702
703        // Not the base slot - search through members
704        for member in &members {
705            let member_slot_offset = U256::from_str(&member.slot).ok()?;
706            let member_slot = struct_start_slot + member_slot_offset;
707            let member_type_info = self.storage_layout.types.get(&member.storage_type)?;
708            let member_label = format!("{}.{}", base_label, member.label);
709
710            // If this member is a struct, recurse into it
711            if is_struct(&member_type_info.label) {
712                let slot_info = self.handle_struct(
713                    &member_label,
714                    member_type_info,
715                    target_slot,
716                    member_slot,
717                    member.offset,
718                    slot_str,
719                    depth + 1,
720                );
721
722                if member_slot == target_slot || slot_info.is_some() {
723                    return slot_info;
724                }
725            }
726
727            if member_slot == target_slot {
728                // Found the exact member slot
729
730                // Regular member
731                let member_type = self.parsed_type(&member.storage_type)?.clone();
732                return Some(SlotInfo {
733                    label: member_label,
734                    slot_type: StorageTypeInfo {
735                        label: member_type_info.label.clone(),
736                        dyn_sol_type: member_type,
737                    },
738                    offset: member.offset,
739                    slot: slot_str.to_string(),
740                    members: None,
741                    decoded: None,
742                    keys: None,
743                });
744            }
745        }
746
747        None
748    }
749
750    /// Handles identification of mapping slots.
751    ///
752    /// Identifies mapping entries by walking up the parent chain to find the base slot,
753    /// then decodes the keys and builds the appropriate label.
754    ///
755    /// # Arguments
756    /// * `storage` - The storage metadata from the layout
757    /// * `storage_type` - Type information for the storage
758    /// * `slot` - The accessed slot being identified
759    /// * `slot_str` - String representation of the slot for output
760    /// * `mapping_slots` - Tracked mapping slot accesses for key resolution
761    fn handle_mapping(
762        &self,
763        storage: &Storage,
764        storage_type: &StorageType,
765        slot: &B256,
766        slot_str: &str,
767        mapping_slots: &MappingSlots,
768    ) -> Option<SlotInfo> {
769        trace!(
770            "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}",
771            storage.slot,
772            slot,
773            mapping_slots.keys.contains_key(slot),
774            mapping_slots.parent_slots.contains_key(slot)
775        );
776
777        // Verify it's actually a mapping type
778        if storage_type.encoding != ENCODING_MAPPING {
779            return None;
780        }
781
782        // Check if this slot is a known mapping entry
783        if !mapping_slots.keys.contains_key(slot) {
784            return None;
785        }
786
787        // Convert storage.slot to B256 for comparison
788        let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?);
789
790        // Walk up the parent chain to collect keys and validate the base slot
791        let mut current_slot = *slot;
792        let mut keys_to_decode = Vec::new();
793        let mut found_base = false;
794
795        while let Some((key, parent)) =
796            mapping_slots.keys.get(&current_slot).zip(mapping_slots.parent_slots.get(&current_slot))
797        {
798            keys_to_decode.push(*key);
799
800            // Check if the parent is our base storage slot
801            if *parent == storage_slot_b256 {
802                found_base = true;
803                break;
804            }
805
806            // Move up to the parent for the next iteration
807            current_slot = *parent;
808        }
809
810        if !found_base {
811            trace!("Mapping slot {} does not match any parent in chain", storage.slot);
812            return None;
813        }
814
815        // Resolve the mapping type to get all key types and the final value type
816        let (key_types, value_type_label, full_type_label) =
817            self.resolve_mapping_type(&storage.storage_type)?;
818
819        // Reverse keys to process from outermost to innermost
820        keys_to_decode.reverse();
821
822        // Build the label with decoded keys and collect decoded key values
823        let mut label = storage.label.clone();
824        let mut decoded_keys = Vec::new();
825
826        // Decode each key using the corresponding type
827        for (i, key) in keys_to_decode.iter().enumerate() {
828            if let Some(key_type_label) = key_types.get(i)
829                && let Some(sol_type) = parse_sol_type(key_type_label)
830                && let Ok(decoded) = sol_type.abi_decode(&key.0)
831            {
832                let decoded_key_str = format_token_raw(&decoded);
833                decoded_keys.push(decoded_key_str.clone());
834                label = format!("{label}[{decoded_key_str}]");
835            } else {
836                let hex_key = hex::encode_prefixed(key.0);
837                decoded_keys.push(hex_key.clone());
838                label = format!("{label}[{hex_key}]");
839            }
840        }
841
842        // Parse the final value type for decoding.
843        // Contract types (e.g., "contract IPool") are addresses under the hood.
844        let dyn_sol_type = parse_sol_type(&value_type_label).unwrap_or(DynSolType::Bytes);
845
846        Some(SlotInfo {
847            label,
848            slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type },
849            offset: storage.offset,
850            slot: slot_str.to_string(),
851            members: None,
852            decoded: None,
853            keys: Some(decoded_keys),
854        })
855    }
856
857    /// Handles identification of bytes/string storage slots.
858    ///
859    /// Bytes and strings in Solidity use a special storage layout:
860    /// - Short values (<32 bytes): stored in the same slot with length * 2
861    /// - Long values (>=32 bytes): length * 2 + 1 in main slot, data at keccak256(slot)
862    ///
863    /// This function checks if the given slot is:
864    /// 1. A main slot for a bytes/string variable
865    /// 2. A data slot for any long bytes/string variable in the storage layout
866    ///
867    /// # Arguments
868    /// * `slot` - The accessed slot being identified
869    /// * `slot_str` - String representation of the slot for output
870    /// * `base_slot_value` - The value at the base slot (used to determine length for long
871    ///   bytes/strings)
872    fn handle_bytes_string(
873        &self,
874        storage: &Storage,
875        storage_type: &StorageType,
876        slot: U256,
877        slot_str: &str,
878        base_slot_value: &B256,
879    ) -> Option<SlotInfo> {
880        // Only handle bytes/string encoded variables for this specific storage entry
881        if storage_type.encoding != ENCODING_BYTES {
882            return None;
883        }
884
885        // Check if this is the main slot for this variable
886        let base_slot = U256::from_str(&storage.slot).ok()?;
887        if slot == base_slot {
888            // Parse the type to get the correct DynSolType
889            let dyn_type = if storage_type.label == "string" {
890                DynSolType::String
891            } else if storage_type.label == "bytes" {
892                DynSolType::Bytes
893            } else {
894                return None;
895            };
896
897            return Some(SlotInfo {
898                label: storage.label.clone(),
899                slot_type: StorageTypeInfo {
900                    label: storage_type.label.clone(),
901                    dyn_sol_type: dyn_type,
902                },
903                offset: storage.offset,
904                slot: slot_str.to_string(),
905                members: None,
906                decoded: None,
907                keys: None,
908            });
909        }
910
911        // Check if it could be a data slot for this long bytes/string
912        // Calculate where data slots would start for this variable
913        let data_start =
914            U256::from_be_bytes(alloy_primitives::keccak256(base_slot.to_be_bytes::<32>()).0);
915
916        // Get the length from the base slot value to calculate exact number of slots
917        // For long bytes/strings, the length is stored as (length * 2 + 1) in the base slot
918        let length_byte = base_slot_value.0[31];
919        if length_byte & 1 == 1 {
920            // It's a long bytes/string
921            let length = U256::from_be_bytes(base_slot_value.0) >> 1;
922            // Calculate number of slots needed (round up)
923            let num_slots = (length + U256::from(31)) / U256::from(32);
924
925            // Check if our slot is within the data region
926            if slot >= data_start && slot < data_start + num_slots {
927                let slot_index = (slot - data_start).try_into().unwrap_or(usize::MAX);
928
929                return Some(SlotInfo {
930                    label: format!("{}[{}]", storage.label, slot_index),
931                    slot_type: StorageTypeInfo {
932                        label: storage_type.label.clone(),
933                        // Type is assigned as FixedBytes(32) for data slots
934                        dyn_sol_type: DynSolType::FixedBytes(32),
935                    },
936                    offset: 0,
937                    slot: slot_str.to_string(),
938                    members: None,
939                    decoded: None,
940                    keys: None,
941                });
942            }
943        }
944
945        None
946    }
947
948    fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec<String>, String, String)> {
949        let storage_type = self.storage_layout.types.get(type_ref)?;
950
951        if storage_type.encoding != ENCODING_MAPPING {
952            // Not a mapping, return the type as-is
953            return Some((vec![], storage_type.label.clone(), storage_type.label.clone()));
954        }
955
956        // Get key and value type references
957        let key_type_ref = storage_type.key.as_ref()?;
958        let value_type_ref = storage_type.value.as_ref()?;
959
960        // Resolve the key type
961        let key_type = self.storage_layout.types.get(key_type_ref)?;
962        let mut key_types = vec![key_type.label.clone()];
963
964        // Check if the value is another mapping (nested case)
965        if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
966            if value_storage_type.encoding == ENCODING_MAPPING {
967                // Recursively resolve the nested mapping
968                let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
969                key_types.extend(nested_keys);
970                return Some((key_types, final_value, storage_type.label.clone()));
971            }
972            // Value is not a mapping, we're done
973            return Some((key_types, value_storage_type.label.clone(), storage_type.label.clone()));
974        }
975
976        None
977    }
978}
979
980/// Returns the base indices for array types, e.g. "\[0\]\[0\]" for 2D arrays.
981fn get_array_base_indices(dyn_type: &DynSolType) -> String {
982    match dyn_type {
983        DynSolType::FixedArray(inner, _) => {
984            if let DynSolType::FixedArray(_, _) = inner.as_ref() {
985                // Nested array (2D or higher)
986                format!("[0]{}", get_array_base_indices(inner))
987            } else {
988                // Simple 1D array
989                "[0]".to_string()
990            }
991        }
992        _ => String::new(),
993    }
994}
995
996/// Parses a storage type label into a [`DynSolType`], returning `None` for labels that have no
997/// direct ABI equivalent (mappings, structs).
998///
999/// Handles `contract X` types (which are addresses under the hood) and `enum X` types
1000/// (which are uint8) that `DynSolType::parse` doesn't recognize.
1001fn parse_sol_type(label: &str) -> Option<DynSolType> {
1002    let scalar = if label.starts_with("contract ") {
1003        "address"
1004    } else if label.starts_with("enum ") {
1005        "uint8"
1006    } else {
1007        return DynSolType::parse(label).ok();
1008    };
1009    let suffix = label.find('[').map_or("", |index| &label[index..]);
1010    DynSolType::parse(&format!("{scalar}{suffix}")).ok()
1011}
1012
1013/// Checks if a given type label represents a struct type.
1014pub fn is_struct(s: &str) -> bool {
1015    s.starts_with("struct ")
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021
1022    #[test]
1023    fn preserves_contract_and_enum_array_dimensions() {
1024        for (label, expected) in [
1025            ("contract IERC20", "address"),
1026            ("contract IERC20[]", "address[]"),
1027            ("contract IERC20[4][]", "address[4][]"),
1028            ("enum Example.Status", "uint8"),
1029            ("enum Example.Status[4]", "uint8[4]"),
1030            ("enum Example.Status[][4]", "uint8[][4]"),
1031        ] {
1032            assert_eq!(parse_sol_type(label), DynSolType::parse(expected).ok(), "{label}");
1033        }
1034    }
1035}