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 = Into::<U256>::into(*base_value) >> 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 = Into::<U256>::into(keccak256(base_slot.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                    if matches!(member.slot_type.dyn_sol_type, DynSolType::Int(_)) {
296                        // ABI signed integers must be sign-extended to a full word.
297                        if previous_value.0[byte_start] & 0x80 != 0 {
298                            prev_bytes.fill(0xff);
299                        }
300                        if new_value.0[byte_start] & 0x80 != 0 {
301                            new_bytes.fill(0xff);
302                        }
303                    }
304                    // ABI fixed bytes are left-aligned, unlike integers and addresses.
305                    let abi_start =
306                        if matches!(member.slot_type.dyn_sol_type, DynSolType::FixedBytes(_)) {
307                            0
308                        } else {
309                            32 - size
310                        };
311                    prev_bytes[abi_start..abi_start + size]
312                        .copy_from_slice(&previous_value.0[byte_start..byte_start + size]);
313                    new_bytes[abi_start..abi_start + size]
314                        .copy_from_slice(&new_value.0[byte_start..byte_start + size]);
315                }
316
317                // Decode the member values
318                if let (Ok(prev_val), Ok(new_val)) = (
319                    member.slot_type.dyn_sol_type.abi_decode(&prev_bytes),
320                    member.slot_type.dyn_sol_type.abi_decode(&new_bytes),
321                ) {
322                    member.decoded =
323                        Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val });
324                }
325            }
326            // For structs with members, we don't need a top-level decoded value
327        } else {
328            // For non-struct types, decode directly
329            // Note: decode() returns None for long bytes/strings, which will be handled by
330            // decode_bytes_or_string()
331            if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) {
332                self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
333            }
334        }
335    }
336}
337
338/// Custom serializer for StorageTypeInfo that only outputs the label
339fn serialize_slot_type<S>(info: &StorageTypeInfo, serializer: S) -> Result<S::Ok, S::Error>
340where
341    S: serde::Serializer,
342{
343    serializer.serialize_str(&info.label)
344}
345
346/// Custom serializer for mapping keys
347fn serialize_mapping_keys<S>(keys: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
348where
349    S: serde::Serializer,
350{
351    use serde::ser::SerializeMap;
352
353    if let Some(keys) = keys {
354        let len = if keys.is_empty() { 0 } else { 1 };
355        let mut map = serializer.serialize_map(Some(len))?;
356        if keys.len() == 1 {
357            map.serialize_entry("key", &keys[0])?;
358        } else if keys.len() > 1 {
359            map.serialize_entry("keys", keys)?;
360        }
361        map.end()
362    } else {
363        serializer.serialize_none()
364    }
365}
366
367/// Decoded storage slot values
368#[derive(Clone, Debug)]
369pub struct DecodedSlotValues {
370    /// Initial decoded storage value
371    pub previous_value: DynSolValue,
372    /// Current decoded storage value
373    pub new_value: DynSolValue,
374}
375
376impl Serialize for DecodedSlotValues {
377    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
378    where
379        S: serde::Serializer,
380    {
381        use serde::ser::SerializeStruct;
382
383        let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?;
384        state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?;
385        state.serialize_field("newValue", &format_token_raw(&self.new_value))?;
386        state.end()
387    }
388}
389
390/// Storage slot identifier that uses Solidity [`StorageLayout`] to identify storage slots.
391#[derive(Clone)]
392pub struct SlotIdentifier {
393    storage_layout: Arc<StorageLayout>,
394    parsed_types: BTreeMap<String, Option<DynSolType>>,
395}
396
397impl SlotIdentifier {
398    /// Creates a new SlotIdentifier with the given storage layout.
399    pub fn new(storage_layout: Arc<StorageLayout>) -> Self {
400        let parsed_types = storage_layout
401            .types
402            .iter()
403            .map(|(id, storage_type)| (id.clone(), parse_sol_type(&storage_type.label)))
404            .collect();
405        Self { storage_layout, parsed_types }
406    }
407
408    fn parsed_type(&self, storage_type: &str) -> Option<&DynSolType> {
409        self.parsed_types.get(storage_type).and_then(Option::as_ref)
410    }
411
412    /// Identifies a storage slots type using the [`StorageLayout`].
413    ///
414    /// It can also identify whether a slot belongs to a mapping if provided with [`MappingSlots`].
415    pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
416        trace!(?slot, "identifying slot");
417        let slot_u256 = Into::<U256>::into(*slot);
418        let slot_str = slot_u256.to_string();
419
420        // Match on a base slot from the layout. This is always the case for primitive types, which
421        // may share the slot with other variables.
422        if let Some(slot_info) = self.slot_variables(
423            self.storage_layout
424                .storage
425                .iter()
426                .filter(|storage| storage.slot == slot_str)
427                .map(|storage| (storage.label.clone(), storage)),
428            &slot_str,
429        ) {
430            return Some(slot_info);
431        }
432
433        for storage in &self.storage_layout.storage {
434            let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
435            let dyn_type = self.parsed_type(&storage.storage_type);
436
437            // Encoding types: <https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#json-output>
438            if storage_type.encoding == ENCODING_INPLACE {
439                // Can be of type FixedArrays or Structs
440                // Handles the case where the accessed `slot` is maybe different from the base slot.
441                let array_start_slot = U256::from_str(&storage.slot).ok()?;
442
443                if let Some(parsed_type) = dyn_type
444                    && let DynSolType::FixedArray(_, _) = parsed_type
445                    && let Some(slot_info) = self.handle_array_slot(
446                        storage,
447                        storage_type,
448                        parsed_type,
449                        slot_u256,
450                        array_start_slot,
451                        &slot_str,
452                    )
453                {
454                    return Some(slot_info);
455                }
456
457                // If type parsing fails and the label is a struct
458                if is_struct(&storage_type.label) {
459                    let struct_start_slot = U256::from_str(&storage.slot).ok()?;
460                    if let Some(slot_info) = self.handle_struct(
461                        &storage.label,
462                        storage_type,
463                        slot_u256,
464                        struct_start_slot,
465                        storage.offset,
466                        &slot_str,
467                        0,
468                    ) {
469                        return Some(slot_info);
470                    }
471                }
472            } else if storage_type.encoding == ENCODING_MAPPING
473                && let Some(mapping_slots) = mapping_slots
474                && let Some(info) =
475                    self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots)
476            {
477                return Some(info);
478            }
479        }
480
481        None
482    }
483
484    /// Identifies a bytes or string storage slot by checking all bytes/string variables
485    /// in the storage layout and using their base slot values from the provided storage changes.
486    ///
487    /// # Arguments
488    /// * `slot` - The slot being identified
489    /// * `storage_values` - Map of storage slots to their current values
490    pub fn identify_bytes_or_string(
491        &self,
492        slot: &B256,
493        storage_values: &B256Map<B256>,
494    ) -> Option<SlotInfo> {
495        let slot_u256 = Into::<U256>::into(*slot);
496        let slot_str = slot_u256.to_string();
497
498        // Search through all bytes/string variables in the storage layout
499        for storage in &self.storage_layout.storage {
500            if let Some(storage_type) = self.storage_layout.types.get(&storage.storage_type)
501                && storage_type.encoding == ENCODING_BYTES
502            {
503                let Some(base_slot) = U256::from_str(&storage.slot).map(B256::from).ok() else {
504                    continue;
505                };
506                // Get the base slot value from storage_values
507                if let Some(base_value) = storage_values.get(&base_slot)
508                    && let Some(info) = self.handle_bytes_string(
509                        storage,
510                        storage_type,
511                        slot_u256,
512                        &slot_str,
513                        base_value,
514                    )
515                {
516                    return Some(info);
517                }
518            }
519        }
520
521        None
522    }
523
524    /// Handles identification of array slots.
525    ///
526    /// # Arguments
527    /// * `storage` - The storage metadata from the layout
528    /// * `storage_type` - Type information for the storage slot
529    /// * `slot` - The target slot being identified
530    /// * `array_start_slot` - The starting slot of the array in storage i.e base_slot
531    /// * `slot_str` - String representation of the slot for output
532    fn handle_array_slot(
533        &self,
534        storage: &Storage,
535        storage_type: &StorageType,
536        parsed_type: &DynSolType,
537        slot: U256,
538        array_start_slot: U256,
539        slot_str: &str,
540    ) -> Option<SlotInfo> {
541        // Check if slot is within array bounds
542        let total_bytes = storage_type.number_of_bytes.parse::<u64>().ok()?;
543        let total_slots = total_bytes.div_ceil(32);
544
545        if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) {
546            let index = (slot - array_start_slot).to::<u64>();
547            // Format the array element label based on array dimensions
548            let label = match parsed_type {
549                DynSolType::FixedArray(inner, _) => {
550                    if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() {
551                        // 2D array: calculate row and column
552                        let row = index / (*inner_size as u64);
553                        let col = index % (*inner_size as u64);
554                        format!("{}[{row}][{col}]", storage.label)
555                    } else {
556                        // 1D array
557                        format!("{}[{index}]", storage.label)
558                    }
559                }
560                _ => storage.label.clone(),
561            };
562
563            return Some(SlotInfo {
564                label,
565                slot_type: StorageTypeInfo {
566                    label: storage_type.label.clone(),
567                    dyn_sol_type: parsed_type.clone(),
568                },
569                offset: 0,
570                slot: slot_str.to_string(),
571                members: None,
572                decoded: None,
573                keys: None,
574            });
575        }
576
577        None
578    }
579
580    /// Handles identification of struct slots.
581    ///
582    /// Recursively resolves struct members to find the exact member corresponding
583    /// to the target slot. Handles both single-slot (packed) and multi-slot structs.
584    ///
585    /// # Arguments
586    /// * `base_label` - The label/name for this struct or member
587    /// * `storage_type` - Type information for the storage
588    /// * `target_slot` - The target slot being identified
589    /// * `struct_start_slot` - The starting slot of this struct
590    /// * `offset` - Offset within the slot (for packed storage)
591    /// * `slot_str` - String representation of the slot for output
592    /// * `depth` - Current recursion depth
593    #[allow(clippy::too_many_arguments)]
594    fn handle_struct(
595        &self,
596        base_label: &str,
597        storage_type: &StorageType,
598        target_slot: U256,
599        struct_start_slot: U256,
600        offset: i64,
601        slot_str: &str,
602        depth: usize,
603    ) -> Option<SlotInfo> {
604        // Limit recursion depth to prevent stack overflow
605        const MAX_DEPTH: usize = 10;
606        if depth > MAX_DEPTH {
607            return None;
608        }
609
610        let members = storage_type
611            .other
612            .get("members")
613            .and_then(|v| serde_json::from_value::<Vec<Storage>>(v.clone()).ok())?;
614
615        // If this is the exact slot we're looking for (struct's base slot)
616        if struct_start_slot == target_slot
617        // Find the member at slot offset 0 (the member that starts at this slot)
618            && let Some(first_member) = members.iter().find(|m| m.slot == "0")
619        {
620            let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?;
621
622            // Check if we have a single-slot struct (all members have slot "0")
623            let is_single_slot = members.iter().all(|m| m.slot == "0");
624
625            if is_single_slot {
626                // Build member info for single-slot struct
627                let mut member_infos = Vec::new();
628                for member in &members {
629                    if let Some(member_type_info) =
630                        self.storage_layout.types.get(&member.storage_type)
631                        && let Some(member_type) = self.parsed_type(&member.storage_type).cloned()
632                    {
633                        member_infos.push(SlotInfo {
634                            label: member.label.clone(),
635                            slot_type: StorageTypeInfo {
636                                label: member_type_info.label.clone(),
637                                dyn_sol_type: member_type,
638                            },
639                            offset: member.offset,
640                            slot: slot_str.to_string(),
641                            members: None,
642                            decoded: None,
643                            keys: None,
644                        });
645                    }
646                }
647
648                // Build the CustomStruct type
649                let struct_name =
650                    storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label);
651                let prop_names: Vec<String> = members.iter().map(|m| m.label.clone()).collect();
652                let member_types: Vec<DynSolType> =
653                    member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect();
654
655                let parsed_type = DynSolType::CustomStruct {
656                    name: struct_name.to_string(),
657                    prop_names,
658                    tuple: member_types,
659                };
660
661                return Some(SlotInfo {
662                    label: base_label.to_string(),
663                    slot_type: StorageTypeInfo {
664                        label: storage_type.label.clone(),
665                        dyn_sol_type: parsed_type,
666                    },
667                    offset,
668                    slot: slot_str.to_string(),
669                    decoded: None,
670                    members: if member_infos.is_empty() { None } else { Some(member_infos) },
671                    keys: None,
672                });
673            }
674
675            // Multi-slot struct - return the first member.
676            let member_label = format!("{}.{}", base_label, first_member.label);
677
678            // If the first member is itself a struct, recurse
679            if is_struct(&member_type_info.label) {
680                return self.handle_struct(
681                    &member_label,
682                    member_type_info,
683                    target_slot,
684                    struct_start_slot,
685                    first_member.offset,
686                    slot_str,
687                    depth + 1,
688                );
689            }
690
691            // Return the primitive members packed into the first slot.
692            return self.slot_variables(
693                members
694                    .iter()
695                    .filter(|m| m.slot == "0")
696                    .map(|m| (format!("{base_label}.{}", m.label), m)),
697                slot_str,
698            );
699        }
700
701        // Not the base slot - search through members
702        for member in &members {
703            let member_slot_offset = U256::from_str(&member.slot).ok()?;
704            let member_slot = struct_start_slot + member_slot_offset;
705            let member_type_info = self.storage_layout.types.get(&member.storage_type)?;
706            let member_label = format!("{}.{}", base_label, member.label);
707
708            // If this member is a struct, recurse into it
709            if is_struct(&member_type_info.label) {
710                let slot_info = self.handle_struct(
711                    &member_label,
712                    member_type_info,
713                    target_slot,
714                    member_slot,
715                    member.offset,
716                    slot_str,
717                    depth + 1,
718                );
719
720                if member_slot == target_slot || slot_info.is_some() {
721                    return slot_info;
722                }
723            }
724
725            if member_slot == target_slot {
726                // Found the exact member slot, which may be shared by packed members.
727                return self.slot_variables(
728                    members
729                        .iter()
730                        .filter(|m| m.slot == member.slot)
731                        .map(|m| (format!("{base_label}.{}", m.label), m)),
732                    slot_str,
733                );
734            }
735        }
736
737        None
738    }
739
740    /// Handles identification of mapping slots.
741    ///
742    /// Identifies mapping entries by walking up the parent chain to find the base slot,
743    /// then decodes the keys and builds the appropriate label.
744    ///
745    /// # Arguments
746    /// * `storage` - The storage metadata from the layout
747    /// * `storage_type` - Type information for the storage
748    /// * `slot` - The accessed slot being identified
749    /// * `slot_str` - String representation of the slot for output
750    /// * `mapping_slots` - Tracked mapping slot accesses for key resolution
751    fn handle_mapping(
752        &self,
753        storage: &Storage,
754        storage_type: &StorageType,
755        slot: &B256,
756        slot_str: &str,
757        mapping_slots: &MappingSlots,
758    ) -> Option<SlotInfo> {
759        trace!(
760            "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}",
761            storage.slot,
762            slot,
763            mapping_slots.keys.contains_key(slot),
764            mapping_slots.parent_slots.contains_key(slot)
765        );
766
767        // Verify it's actually a mapping type
768        if storage_type.encoding != ENCODING_MAPPING {
769            return None;
770        }
771
772        // Check if this slot is a known mapping entry
773        if !mapping_slots.keys.contains_key(slot) {
774            return None;
775        }
776
777        // Convert storage.slot to B256 for comparison
778        let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?);
779
780        // Walk up the parent chain to collect keys and validate the base slot
781        let mut current_slot = *slot;
782        let mut keys_to_decode = Vec::new();
783        let mut found_base = false;
784
785        while let Some((key, parent)) =
786            mapping_slots.keys.get(&current_slot).zip(mapping_slots.parent_slots.get(&current_slot))
787        {
788            keys_to_decode.push(*key);
789
790            // Check if the parent is our base storage slot
791            if *parent == storage_slot_b256 {
792                found_base = true;
793                break;
794            }
795
796            // Move up to the parent for the next iteration
797            current_slot = *parent;
798        }
799
800        if !found_base {
801            trace!("Mapping slot {} does not match any parent in chain", storage.slot);
802            return None;
803        }
804
805        // Resolve the mapping type to get all key types and the final value type
806        let (key_types, value_type_label, full_type_label) =
807            self.resolve_mapping_type(&storage.storage_type)?;
808
809        // Reverse keys to process from outermost to innermost
810        keys_to_decode.reverse();
811
812        // Build the label with decoded keys and collect decoded key values
813        let mut label = storage.label.clone();
814        let mut decoded_keys = Vec::new();
815
816        // Decode each key using the corresponding type
817        for (i, key) in keys_to_decode.iter().enumerate() {
818            if let Some(key_type_label) = key_types.get(i)
819                && let Some(sol_type) = parse_sol_type(key_type_label)
820                && let Ok(decoded) = sol_type.abi_decode(&key.0)
821            {
822                let decoded_key_str = format_token_raw(&decoded);
823                decoded_keys.push(decoded_key_str.clone());
824                label = format!("{label}[{decoded_key_str}]");
825            } else {
826                let hex_key = hex::encode_prefixed(key.0);
827                decoded_keys.push(hex_key.clone());
828                label = format!("{label}[{hex_key}]");
829            }
830        }
831
832        // Parse the final value type for decoding.
833        // Contract types (e.g., "contract IPool") are addresses under the hood.
834        let dyn_sol_type = parse_sol_type(&value_type_label).unwrap_or(DynSolType::Bytes);
835
836        Some(SlotInfo {
837            label,
838            slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type },
839            offset: storage.offset,
840            slot: slot_str.to_string(),
841            members: None,
842            decoded: None,
843            keys: Some(decoded_keys),
844        })
845    }
846
847    /// Handles identification of bytes/string storage slots.
848    ///
849    /// Bytes and strings in Solidity use a special storage layout:
850    /// - Short values (<32 bytes): stored in the same slot with length * 2
851    /// - Long values (>=32 bytes): length * 2 + 1 in main slot, data at keccak256(slot)
852    ///
853    /// This function checks if the given slot is:
854    /// 1. A main slot for a bytes/string variable
855    /// 2. A data slot for any long bytes/string variable in the storage layout
856    ///
857    /// # Arguments
858    /// * `slot` - The accessed slot being identified
859    /// * `slot_str` - String representation of the slot for output
860    /// * `base_slot_value` - The value at the base slot (used to determine length for long
861    ///   bytes/strings)
862    fn handle_bytes_string(
863        &self,
864        storage: &Storage,
865        storage_type: &StorageType,
866        slot: U256,
867        slot_str: &str,
868        base_slot_value: &B256,
869    ) -> Option<SlotInfo> {
870        // Only handle bytes/string encoded variables for this specific storage entry
871        if storage_type.encoding != ENCODING_BYTES {
872            return None;
873        }
874
875        // Check if this is the main slot for this variable
876        let base_slot = U256::from_str(&storage.slot).ok()?;
877        if slot == base_slot {
878            // Parse the type to get the correct DynSolType
879            let dyn_type = if storage_type.label == "string" {
880                DynSolType::String
881            } else if storage_type.label == "bytes" {
882                DynSolType::Bytes
883            } else {
884                return None;
885            };
886
887            return Some(SlotInfo {
888                label: storage.label.clone(),
889                slot_type: StorageTypeInfo {
890                    label: storage_type.label.clone(),
891                    dyn_sol_type: dyn_type,
892                },
893                offset: storage.offset,
894                slot: slot_str.to_string(),
895                members: None,
896                decoded: None,
897                keys: None,
898            });
899        }
900
901        // Check if it could be a data slot for this long bytes/string
902        // Calculate where data slots would start for this variable
903        let data_start =
904            Into::<U256>::into(alloy_primitives::keccak256(base_slot.to_be_bytes::<32>()));
905
906        // Get the length from the base slot value to calculate exact number of slots
907        // For long bytes/strings, the length is stored as (length * 2 + 1) in the base slot
908        let length_byte = base_slot_value.0[31];
909        if length_byte & 1 == 1 {
910            // It's a long bytes/string
911            let length = Into::<U256>::into(*base_slot_value) >> 1;
912            // Calculate number of slots needed (round up)
913            let num_slots = (length + U256::from(31)) / U256::from(32);
914
915            // Check if our slot is within the data region
916            if slot >= data_start && slot < data_start + num_slots {
917                let slot_index = (slot - data_start).try_into().unwrap_or(usize::MAX);
918
919                return Some(SlotInfo {
920                    label: format!("{}[{}]", storage.label, slot_index),
921                    slot_type: StorageTypeInfo {
922                        label: storage_type.label.clone(),
923                        // Type is assigned as FixedBytes(32) for data slots
924                        dyn_sol_type: DynSolType::FixedBytes(32),
925                    },
926                    offset: 0,
927                    slot: slot_str.to_string(),
928                    members: None,
929                    decoded: None,
930                    keys: None,
931                });
932            }
933        }
934
935        None
936    }
937
938    fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec<String>, String, String)> {
939        let storage_type = self.storage_layout.types.get(type_ref)?;
940
941        if storage_type.encoding != ENCODING_MAPPING {
942            // Not a mapping, return the type as-is
943            return Some((vec![], storage_type.label.clone(), storage_type.label.clone()));
944        }
945
946        // Get key and value type references
947        let key_type_ref = storage_type.key.as_ref()?;
948        let value_type_ref = storage_type.value.as_ref()?;
949
950        // Resolve the key type
951        let key_type = self.storage_layout.types.get(key_type_ref)?;
952        let mut key_types = vec![key_type.label.clone()];
953
954        // Check if the value is another mapping (nested case)
955        if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
956            if value_storage_type.encoding == ENCODING_MAPPING {
957                // Recursively resolve the nested mapping
958                let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
959                key_types.extend(nested_keys);
960                return Some((key_types, final_value, storage_type.label.clone()));
961            }
962            // Value is not a mapping, we're done
963            return Some((key_types, value_storage_type.label.clone(), storage_type.label.clone()));
964        }
965
966        None
967    }
968
969    /// Identifies a slot holding the ABI-decodable variables in `entries`, all stored in it.
970    ///
971    /// A slot shared by several packed variables gets one member per variable, so each is decoded
972    /// from its own bytes and labeled with its own name. Returns `None` if there are no such
973    /// variables.
974    fn slot_variables<'a>(
975        &self,
976        entries: impl IntoIterator<Item = (String, &'a Storage)>,
977        slot_str: &str,
978    ) -> Option<SlotInfo> {
979        let mut variables = entries
980            .into_iter()
981            .filter_map(|(label, storage)| {
982                let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
983                let parsed_type = self.parsed_type(&storage.storage_type)?.clone();
984                let label = if let DynSolType::FixedArray(_, _) = &parsed_type {
985                    format!("{label}{}", get_array_base_indices(&parsed_type))
986                } else {
987                    label
988                };
989                Some(SlotInfo {
990                    label,
991                    slot_type: StorageTypeInfo {
992                        label: storage_type.label.clone(),
993                        dyn_sol_type: parsed_type,
994                    },
995                    offset: storage.offset,
996                    slot: slot_str.to_string(),
997                    members: None,
998                    decoded: None,
999                    keys: None,
1000                })
1001            })
1002            .collect::<Vec<_>>();
1003        if variables.len() <= 1 {
1004            return variables.pop();
1005        }
1006
1007        let label = variables.iter().map(|v| v.label.as_str()).collect::<Vec<_>>().join(", ");
1008        let types = variables.iter().map(|v| v.slot_type.dyn_sol_type.clone()).collect::<Vec<_>>();
1009        let tuple = DynSolType::Tuple(types);
1010        Some(SlotInfo {
1011            label,
1012            slot_type: StorageTypeInfo { label: tuple.to_string(), dyn_sol_type: tuple },
1013            offset: 0,
1014            slot: slot_str.to_string(),
1015            members: Some(variables),
1016            decoded: None,
1017            keys: None,
1018        })
1019    }
1020}
1021
1022/// Returns the base indices for array types, e.g. "\[0\]\[0\]" for 2D arrays.
1023fn get_array_base_indices(dyn_type: &DynSolType) -> String {
1024    match dyn_type {
1025        DynSolType::FixedArray(inner, _) => {
1026            if let DynSolType::FixedArray(_, _) = inner.as_ref() {
1027                // Nested array (2D or higher)
1028                format!("[0]{}", get_array_base_indices(inner))
1029            } else {
1030                // Simple 1D array
1031                "[0]".to_string()
1032            }
1033        }
1034        _ => String::new(),
1035    }
1036}
1037
1038/// Parses a storage type label into a [`DynSolType`], returning `None` for labels that have no
1039/// direct ABI equivalent (mappings, structs).
1040///
1041/// Handles `contract X` types (which are addresses under the hood) and `enum X` types
1042/// (which are uint8) that `DynSolType::parse` doesn't recognize.
1043fn parse_sol_type(label: &str) -> Option<DynSolType> {
1044    let scalar = if label.starts_with("contract ") {
1045        "address"
1046    } else if label.starts_with("enum ") {
1047        "uint8"
1048    } else {
1049        return DynSolType::parse(label).ok();
1050    };
1051    let suffix = label.find('[').map_or("", |index| &label[index..]);
1052    DynSolType::parse(&format!("{scalar}{suffix}")).ok()
1053}
1054
1055/// Checks if a given type label represents a struct type.
1056pub fn is_struct(s: &str) -> bool {
1057    s.starts_with("struct ")
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063
1064    #[test]
1065    fn preserves_contract_and_enum_array_dimensions() {
1066        for (label, expected) in [
1067            ("contract IERC20", "address"),
1068            ("contract IERC20[]", "address[]"),
1069            ("contract IERC20[4][]", "address[4][]"),
1070            ("enum Example.Status", "uint8"),
1071            ("enum Example.Status[4]", "uint8[4]"),
1072            ("enum Example.Status[][4]", "uint8[][4]"),
1073        ] {
1074            assert_eq!(parse_sol_type(label), DynSolType::parse(expected).ok(), "{label}");
1075        }
1076    }
1077
1078    fn decoded(info: &SlotInfo) -> Vec<(&str, String, String)> {
1079        info.members
1080            .iter()
1081            .flatten()
1082            .map(|member| {
1083                let decoded = member.decoded.as_ref().unwrap();
1084                (
1085                    member.label.as_str(),
1086                    format_token_raw(&decoded.previous_value),
1087                    format_token_raw(&decoded.new_value),
1088                )
1089            })
1090            .collect()
1091    }
1092
1093    #[test]
1094    fn decodes_each_packed_variable() {
1095        let layout = serde_json::from_value::<StorageLayout>(serde_json::json!({
1096            "storage": [
1097                { "astId": 1, "contract": "C", "label": "initialized", "offset": 0, "slot": "5", "type": "t_uint64" },
1098                { "astId": 2, "contract": "C", "label": "initializing", "offset": 8, "slot": "5", "type": "t_bool" },
1099                { "astId": 3, "contract": "C", "label": "next", "offset": 0, "slot": "6", "type": "t_uint64" }
1100            ],
1101            "types": {
1102                "t_uint64": { "encoding": "inplace", "label": "uint64", "numberOfBytes": "8" },
1103                "t_bool": { "encoding": "inplace", "label": "bool", "numberOfBytes": "1" }
1104            }
1105        }))
1106        .unwrap();
1107        let identifier = SlotIdentifier::new(Arc::new(layout));
1108
1109        let mut info = identifier.identify(&B256::from(U256::from(5)), None).unwrap();
1110        assert_eq!(info.label, "initialized, initializing");
1111        assert_eq!(info.slot_type.label, "(uint64,bool)");
1112        let new_value = B256::from(U256::from(1) << 64 | U256::from(7));
1113        info.decode_values(B256::ZERO, new_value);
1114        assert_eq!(
1115            decoded(&info),
1116            [
1117                ("initialized", "0".to_string(), "7".to_string()),
1118                ("initializing", "false".to_string(), "true".to_string())
1119            ]
1120        );
1121
1122        let info = identifier.identify(&B256::from(U256::from(6)), None).unwrap();
1123        assert_eq!(info.label, "next");
1124        assert!(info.members.is_none());
1125    }
1126
1127    #[test]
1128    fn decodes_packed_struct_members_past_the_first_slot() {
1129        let layout = serde_json::from_value::<StorageLayout>(serde_json::json!({
1130            "storage": [
1131                { "astId": 1, "contract": "C", "label": "data", "offset": 0, "slot": "0", "type": "t_struct(Data)" }
1132            ],
1133            "types": {
1134                "t_struct(Data)": {
1135                    "encoding": "inplace", "label": "struct C.Data", "numberOfBytes": "64",
1136                    "members": [
1137                        { "astId": 2, "contract": "C", "label": "total", "offset": 0, "slot": "0", "type": "t_uint256" },
1138                        { "astId": 3, "contract": "C", "label": "owner", "offset": 0, "slot": "1", "type": "t_address" },
1139                        { "astId": 4, "contract": "C", "label": "paused", "offset": 20, "slot": "1", "type": "t_bool" }
1140                    ]
1141                },
1142                "t_uint256": { "encoding": "inplace", "label": "uint256", "numberOfBytes": "32" },
1143                "t_address": { "encoding": "inplace", "label": "address", "numberOfBytes": "20" },
1144                "t_bool": { "encoding": "inplace", "label": "bool", "numberOfBytes": "1" }
1145            }
1146        }))
1147        .unwrap();
1148        let identifier = SlotIdentifier::new(Arc::new(layout));
1149
1150        let info = identifier.identify(&B256::ZERO, None).unwrap();
1151        assert_eq!(info.label, "data.total");
1152
1153        let mut info = identifier.identify(&B256::from(U256::from(1)), None).unwrap();
1154        assert_eq!(info.label, "data.owner, data.paused");
1155        let new_value = B256::from(U256::from(1) << 160 | U256::from(0x1234));
1156        info.decode_values(B256::ZERO, new_value);
1157        assert_eq!(
1158            decoded(&info),
1159            [
1160                (
1161                    "data.owner",
1162                    "0x0000000000000000000000000000000000000000".to_string(),
1163                    "0x0000000000000000000000000000000000001234".to_string()
1164                ),
1165                ("data.paused", "false".to_string(), "true".to_string())
1166            ]
1167        );
1168    }
1169
1170    #[test]
1171    fn decodes_packed_signed_variables() {
1172        let layout = serde_json::from_value::<StorageLayout>(serde_json::json!({
1173            "storage": [
1174                { "astId": 1, "contract": "C", "label": "a", "offset": 0, "slot": "0", "type": "t_int8" },
1175                { "astId": 2, "contract": "C", "label": "b", "offset": 1, "slot": "0", "type": "t_int8" }
1176            ],
1177            "types": {
1178                "t_int8": { "encoding": "inplace", "label": "int8", "numberOfBytes": "1" }
1179            }
1180        }))
1181        .unwrap();
1182        let identifier = SlotIdentifier::new(Arc::new(layout));
1183        let mut info = identifier.identify(&B256::ZERO, None).unwrap();
1184
1185        info.decode_values(B256::from(U256::from(0x7fff)), B256::from(U256::from(0x80fe)));
1186        assert_eq!(
1187            decoded(&info),
1188            [
1189                ("a", "-1".to_string(), "-2".to_string()),
1190                ("b", "127".to_string(), "-128".to_string())
1191            ]
1192        );
1193
1194        info.decode_values(B256::from(U256::from(0x80fe)), B256::from(U256::from(0xff7f)));
1195        assert_eq!(
1196            decoded(&info),
1197            [
1198                ("a", "-2".to_string(), "127".to_string()),
1199                ("b", "-128".to_string(), "-1".to_string())
1200            ]
1201        );
1202    }
1203
1204    #[test]
1205    fn decodes_packed_fixed_bytes() {
1206        let layout = serde_json::from_value::<StorageLayout>(serde_json::json!({
1207            "storage": [
1208                { "astId": 1, "contract": "C", "label": "counter", "offset": 0, "slot": "0", "type": "t_uint8" },
1209                { "astId": 2, "contract": "C", "label": "tag", "offset": 1, "slot": "0", "type": "t_bytes1" },
1210                { "astId": 3, "contract": "C", "label": "hash", "offset": 2, "slot": "0", "type": "t_bytes16" }
1211            ],
1212            "types": {
1213                "t_uint8": { "encoding": "inplace", "label": "uint8", "numberOfBytes": "1" },
1214                "t_bytes1": { "encoding": "inplace", "label": "bytes1", "numberOfBytes": "1" },
1215                "t_bytes16": { "encoding": "inplace", "label": "bytes16", "numberOfBytes": "16" }
1216            }
1217        }))
1218        .unwrap();
1219        let identifier = SlotIdentifier::new(Arc::new(layout));
1220        let mut info = identifier.identify(&B256::ZERO, None).unwrap();
1221        let previous =
1222            U256::from(0x0123456789abcdef0123456789abcdef_u128) << 16 | U256::from(0xab07);
1223        let new = U256::from(0xfedcba9876543210fedcba9876543210_u128) << 16 | U256::from(0xcd0b);
1224        info.decode_values(B256::from(previous), B256::from(new));
1225        assert_eq!(
1226            decoded(&info),
1227            [
1228                ("counter", "7".to_string(), "11".to_string()),
1229                ("tag", "0xab".to_string(), "0xcd".to_string()),
1230                (
1231                    "hash",
1232                    "0x0123456789abcdef0123456789abcdef".to_string(),
1233                    "0xfedcba9876543210fedcba9876543210".to_string()
1234                )
1235            ]
1236        );
1237    }
1238}