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};
9use foundry_common_fmt::format_token_raw;
10use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType};
11use serde::Serialize;
12use std::{str::FromStr, sync::Arc};
13use tracing::trace;
14
15// Constants for storage type encodings
16const ENCODING_INPLACE: &str = "inplace";
17const ENCODING_MAPPING: &str = "mapping";
18
19/// Information about a storage slot including its label, type, and decoded values.
20#[derive(Serialize, Debug)]
21pub struct SlotInfo {
22    /// The variable name from the storage layout.
23    ///
24    /// For top-level variables: just the variable name (e.g., "myVariable")
25    /// For struct members: dotted path (e.g., "myStruct.memberName")
26    /// For array elements: name with indices (e.g., "myArray\[0\]", "matrix\[1\]\[2\]")
27    /// For nested structures: full path (e.g., "outer.inner.field")
28    /// For mappings: base name with keys (e.g., "balances\[0x1234...\]")/ex
29    pub label: String,
30    /// The Solidity type information
31    #[serde(rename = "type", serialize_with = "serialize_slot_type")]
32    pub slot_type: StorageTypeInfo,
33    /// Offset within the storage slot (for packed storage)
34    pub offset: i64,
35    /// The storage slot number as a string
36    pub slot: String,
37    /// For struct members, contains nested SlotInfo for each member
38    ///
39    /// This is populated when a struct's members / fields are packed in a single slot.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub members: Option<Vec<SlotInfo>>,
42    /// Decoded values (if available) - used for struct members
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub decoded: Option<DecodedSlotValues>,
45    /// Decoded mapping keys (serialized as "key" for single, "keys" for multiple)
46    #[serde(
47        skip_serializing_if = "Option::is_none",
48        flatten,
49        serialize_with = "serialize_mapping_keys"
50    )]
51    pub keys: Option<Vec<String>>,
52}
53
54/// Wrapper type that holds both the original type label and the parsed DynSolType.
55///
56/// We need both because:
57/// - `label`: Used for serialization to ensure output matches user expectations
58/// - `dyn_sol_type`: The parsed type used for actual value decoding
59#[derive(Debug)]
60pub struct StorageTypeInfo {
61    /// The original type label from storage layout (e.g., "uint256", "address", "mapping(address
62    /// => uint256)")
63    pub label: String,
64    /// The parsed dynamic Solidity type used for decoding
65    pub dyn_sol_type: DynSolType,
66}
67
68impl SlotInfo {
69    /// Decodes a single storage value based on the slot's type information.
70    pub fn decode(&self, value: B256) -> Option<DynSolValue> {
71        // Storage values are always 32 bytes, stored as a single word
72        let mut actual_type = &self.slot_type.dyn_sol_type;
73        // Unwrap nested arrays to get to the base element type.
74        while let DynSolType::FixedArray(elem_type, _) = actual_type {
75            actual_type = elem_type.as_ref();
76        }
77
78        // Decode based on the actual type
79        actual_type.abi_decode(&value.0).ok()
80    }
81
82    /// Decodes storage values (previous and new) and populates the decoded field.
83    /// For structs with members, it decodes each member individually.
84    pub fn decode_values(&mut self, previous_value: B256, new_value: B256) {
85        // If this is a struct with members, decode each member individually
86        if let Some(members) = &mut self.members {
87            for member in members.iter_mut() {
88                let offset = member.offset as usize;
89                let size = match &member.slot_type.dyn_sol_type {
90                    DynSolType::Uint(bits) | DynSolType::Int(bits) => bits / 8,
91                    DynSolType::Address => 20,
92                    DynSolType::Bool => 1,
93                    DynSolType::FixedBytes(size) => *size,
94                    _ => 32, // Default to full word
95                };
96
97                // Extract and decode member values
98                let mut prev_bytes = [0u8; 32];
99                let mut new_bytes = [0u8; 32];
100
101                if offset + size <= 32 {
102                    // In Solidity storage, values are right-aligned
103                    // For offset 0, we want the rightmost bytes
104                    // For offset 16 (for a uint128), we want bytes 0-16
105                    // For packed storage: offset 0 is at the rightmost position
106                    // offset 0, size 16 -> read bytes 16-32 (rightmost)
107                    // offset 16, size 16 -> read bytes 0-16 (leftmost)
108                    let byte_start = 32 - offset - size;
109                    prev_bytes[32 - size..]
110                        .copy_from_slice(&previous_value.0[byte_start..byte_start + size]);
111                    new_bytes[32 - size..]
112                        .copy_from_slice(&new_value.0[byte_start..byte_start + size]);
113                }
114
115                // Decode the member values
116                if let (Ok(prev_val), Ok(new_val)) = (
117                    member.slot_type.dyn_sol_type.abi_decode(&prev_bytes),
118                    member.slot_type.dyn_sol_type.abi_decode(&new_bytes),
119                ) {
120                    member.decoded =
121                        Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val });
122                }
123            }
124            // For structs with members, we don't need a top-level decoded value
125        } else {
126            // For non-struct types, decode directly
127            if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) {
128                self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
129            }
130        }
131    }
132}
133
134/// Custom serializer for StorageTypeInfo that only outputs the label
135fn serialize_slot_type<S>(info: &StorageTypeInfo, serializer: S) -> Result<S::Ok, S::Error>
136where
137    S: serde::Serializer,
138{
139    serializer.serialize_str(&info.label)
140}
141
142/// Custom serializer for mapping keys
143fn serialize_mapping_keys<S>(keys: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
144where
145    S: serde::Serializer,
146{
147    use serde::ser::SerializeMap;
148
149    if let Some(keys) = keys {
150        let mut map = serializer.serialize_map(Some(1))?;
151        if keys.len() == 1 {
152            map.serialize_entry("key", &keys[0])?;
153        } else if keys.len() > 1 {
154            map.serialize_entry("keys", keys)?;
155        }
156        map.end()
157    } else {
158        serializer.serialize_none()
159    }
160}
161
162/// Decoded storage slot values
163#[derive(Debug)]
164pub struct DecodedSlotValues {
165    /// Initial decoded storage value
166    pub previous_value: DynSolValue,
167    /// Current decoded storage value
168    pub new_value: DynSolValue,
169}
170
171impl Serialize for DecodedSlotValues {
172    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173    where
174        S: serde::Serializer,
175    {
176        use serde::ser::SerializeStruct;
177
178        let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?;
179        state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?;
180        state.serialize_field("newValue", &format_token_raw(&self.new_value))?;
181        state.end()
182    }
183}
184
185/// Storage slot identifier that uses Solidity [`StorageLayout`] to identify storage slots.
186pub struct SlotIdentifier {
187    storage_layout: Arc<StorageLayout>,
188}
189
190impl SlotIdentifier {
191    /// Creates a new SlotIdentifier with the given storage layout.
192    pub fn new(storage_layout: Arc<StorageLayout>) -> Self {
193        Self { storage_layout }
194    }
195
196    /// Identifies a storage slots type using the [`StorageLayout`].
197    ///
198    /// It can also identify whether a slot belongs to a mapping if provided with [`MappingSlots`].
199    pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
200        trace!(?slot, "identifying slot");
201        let slot_u256 = U256::from_be_bytes(slot.0);
202        let slot_str = slot_u256.to_string();
203
204        for storage in &self.storage_layout.storage {
205            let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
206            let dyn_type = DynSolType::parse(&storage_type.label).ok();
207
208            // Check if we're able to match on a slot from the layout i.e any of the base slots.
209            // This will always be the case for primitive types that fit in a single slot.
210            if storage.slot == slot_str
211                && let Some(parsed_type) = dyn_type
212            {
213                // Successfully parsed - handle arrays or simple types
214                let label = if let DynSolType::FixedArray(_, _) = &parsed_type {
215                    format!("{}{}", storage.label, get_array_base_indices(&parsed_type))
216                } else {
217                    storage.label.clone()
218                };
219
220                return Some(SlotInfo {
221                    label,
222                    slot_type: StorageTypeInfo {
223                        label: storage_type.label.clone(),
224                        dyn_sol_type: parsed_type,
225                    },
226                    offset: storage.offset,
227                    slot: storage.slot.clone(),
228                    members: None,
229                    decoded: None,
230                    keys: None,
231                });
232            }
233
234            // Encoding types: <https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#json-output>
235            if storage_type.encoding == ENCODING_INPLACE {
236                // Can be of type FixedArrays or Structs
237                // Handles the case where the accessed `slot` is maybe different from the base slot.
238                let array_start_slot = U256::from_str(&storage.slot).ok()?;
239
240                if let Some(parsed_type) = dyn_type
241                    && let DynSolType::FixedArray(_, _) = parsed_type
242                    && let Some(slot_info) = self.handle_array_slot(
243                        storage,
244                        storage_type,
245                        slot_u256,
246                        array_start_slot,
247                        &slot_str,
248                    )
249                {
250                    return Some(slot_info);
251                }
252
253                // If type parsing fails and the label is a struct
254                if is_struct(&storage_type.label) {
255                    let struct_start_slot = U256::from_str(&storage.slot).ok()?;
256                    if let Some(slot_info) = self.handle_struct(
257                        &storage.label,
258                        storage_type,
259                        slot_u256,
260                        struct_start_slot,
261                        storage.offset,
262                        &slot_str,
263                        0,
264                    ) {
265                        return Some(slot_info);
266                    }
267                }
268            } else if storage_type.encoding == ENCODING_MAPPING
269                && let Some(mapping_slots) = mapping_slots
270                && let Some(slot_info) =
271                    self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots)
272            {
273                return Some(slot_info);
274            }
275        }
276
277        None
278    }
279
280    /// Handles identification of array slots.
281    ///
282    /// # Arguments
283    /// * `storage` - The storage metadata from the layout
284    /// * `storage_type` - Type information for the storage slot
285    /// * `slot` - The target slot being identified
286    /// * `array_start_slot` - The starting slot of the array in storage i.e base_slot
287    /// * `slot_str` - String representation of the slot for output
288    fn handle_array_slot(
289        &self,
290        storage: &Storage,
291        storage_type: &StorageType,
292        slot: U256,
293        array_start_slot: U256,
294        slot_str: &str,
295    ) -> Option<SlotInfo> {
296        // Check if slot is within array bounds
297        let total_bytes = storage_type.number_of_bytes.parse::<u64>().ok()?;
298        let total_slots = total_bytes.div_ceil(32);
299
300        if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) {
301            let parsed_type = DynSolType::parse(&storage_type.label).ok()?;
302            let index = (slot - array_start_slot).to::<u64>();
303            // Format the array element label based on array dimensions
304            let label = match &parsed_type {
305                DynSolType::FixedArray(inner, _) => {
306                    if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() {
307                        // 2D array: calculate row and column
308                        let row = index / (*inner_size as u64);
309                        let col = index % (*inner_size as u64);
310                        format!("{}[{row}][{col}]", storage.label)
311                    } else {
312                        // 1D array
313                        format!("{}[{index}]", storage.label)
314                    }
315                }
316                _ => storage.label.clone(),
317            };
318
319            return Some(SlotInfo {
320                label,
321                slot_type: StorageTypeInfo {
322                    label: storage_type.label.clone(),
323                    dyn_sol_type: parsed_type,
324                },
325                offset: 0,
326                slot: slot_str.to_string(),
327                members: None,
328                decoded: None,
329                keys: None,
330            });
331        }
332
333        None
334    }
335
336    /// Handles identification of struct slots.
337    ///
338    /// Recursively resolves struct members to find the exact member corresponding
339    /// to the target slot. Handles both single-slot (packed) and multi-slot structs.
340    ///
341    /// # Arguments
342    /// * `base_label` - The label/name for this struct or member
343    /// * `storage_type` - Type information for the storage
344    /// * `target_slot` - The target slot being identified
345    /// * `struct_start_slot` - The starting slot of this struct
346    /// * `offset` - Offset within the slot (for packed storage)
347    /// * `slot_str` - String representation of the slot for output
348    /// * `depth` - Current recursion depth
349    #[allow(clippy::too_many_arguments)]
350    fn handle_struct(
351        &self,
352        base_label: &str,
353        storage_type: &StorageType,
354        target_slot: U256,
355        struct_start_slot: U256,
356        offset: i64,
357        slot_str: &str,
358        depth: usize,
359    ) -> Option<SlotInfo> {
360        // Limit recursion depth to prevent stack overflow
361        const MAX_DEPTH: usize = 10;
362        if depth > MAX_DEPTH {
363            return None;
364        }
365
366        let members = storage_type
367            .other
368            .get("members")
369            .and_then(|v| serde_json::from_value::<Vec<Storage>>(v.clone()).ok())?;
370
371        // If this is the exact slot we're looking for (struct's base slot)
372        if struct_start_slot == target_slot
373        // Find the member at slot offset 0 (the member that starts at this slot)
374            && let Some(first_member) = members.iter().find(|m| m.slot == "0")
375        {
376            let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?;
377
378            // Check if we have a single-slot struct (all members have slot "0")
379            let is_single_slot = members.iter().all(|m| m.slot == "0");
380
381            if is_single_slot {
382                // Build member info for single-slot struct
383                let mut member_infos = Vec::new();
384                for member in &members {
385                    if let Some(member_type_info) =
386                        self.storage_layout.types.get(&member.storage_type)
387                        && let Some(member_type) = DynSolType::parse(&member_type_info.label).ok()
388                    {
389                        member_infos.push(SlotInfo {
390                            label: member.label.clone(),
391                            slot_type: StorageTypeInfo {
392                                label: member_type_info.label.clone(),
393                                dyn_sol_type: member_type,
394                            },
395                            offset: member.offset,
396                            slot: slot_str.to_string(),
397                            members: None,
398                            decoded: None,
399                            keys: None,
400                        });
401                    }
402                }
403
404                // Build the CustomStruct type
405                let struct_name =
406                    storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label);
407                let prop_names: Vec<String> = members.iter().map(|m| m.label.clone()).collect();
408                let member_types: Vec<DynSolType> =
409                    member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect();
410
411                let parsed_type = DynSolType::CustomStruct {
412                    name: struct_name.to_string(),
413                    prop_names,
414                    tuple: member_types,
415                };
416
417                return Some(SlotInfo {
418                    label: base_label.to_string(),
419                    slot_type: StorageTypeInfo {
420                        label: storage_type.label.clone(),
421                        dyn_sol_type: parsed_type,
422                    },
423                    offset,
424                    slot: slot_str.to_string(),
425                    decoded: None,
426                    members: if member_infos.is_empty() { None } else { Some(member_infos) },
427                    keys: None,
428                });
429            } else {
430                // Multi-slot struct - return the first member.
431                let member_label = format!("{}.{}", base_label, first_member.label);
432
433                // If the first member is itself a struct, recurse
434                if is_struct(&member_type_info.label) {
435                    return self.handle_struct(
436                        &member_label,
437                        member_type_info,
438                        target_slot,
439                        struct_start_slot,
440                        first_member.offset,
441                        slot_str,
442                        depth + 1,
443                    );
444                }
445
446                // Return the first member as a primitive
447                return Some(SlotInfo {
448                    label: member_label,
449                    slot_type: StorageTypeInfo {
450                        label: member_type_info.label.clone(),
451                        dyn_sol_type: DynSolType::parse(&member_type_info.label).ok()?,
452                    },
453                    offset: first_member.offset,
454                    slot: slot_str.to_string(),
455                    decoded: None,
456                    members: None,
457                    keys: None,
458                });
459            }
460        }
461
462        // Not the base slot - search through members
463        for member in &members {
464            let member_slot_offset = U256::from_str(&member.slot).ok()?;
465            let member_slot = struct_start_slot + member_slot_offset;
466            let member_type_info = self.storage_layout.types.get(&member.storage_type)?;
467            let member_label = format!("{}.{}", base_label, member.label);
468
469            // If this member is a struct, recurse into it
470            if is_struct(&member_type_info.label) {
471                let slot_info = self.handle_struct(
472                    &member_label,
473                    member_type_info,
474                    target_slot,
475                    member_slot,
476                    member.offset,
477                    slot_str,
478                    depth + 1,
479                );
480
481                if member_slot == target_slot || slot_info.is_some() {
482                    return slot_info;
483                }
484            }
485
486            if member_slot == target_slot {
487                // Found the exact member slot
488
489                // Regular member
490                let member_type = DynSolType::parse(&member_type_info.label).ok()?;
491                return Some(SlotInfo {
492                    label: member_label,
493                    slot_type: StorageTypeInfo {
494                        label: member_type_info.label.clone(),
495                        dyn_sol_type: member_type,
496                    },
497                    offset: member.offset,
498                    slot: slot_str.to_string(),
499                    members: None,
500                    decoded: None,
501                    keys: None,
502                });
503            }
504        }
505
506        None
507    }
508
509    /// Handles identification of mapping slots.
510    ///
511    /// Identifies mapping entries by walking up the parent chain to find the base slot,
512    /// then decodes the keys and builds the appropriate label.
513    ///
514    /// # Arguments
515    /// * `storage` - The storage metadata from the layout
516    /// * `storage_type` - Type information for the storage
517    /// * `slot` - The accessed slot being identified
518    /// * `slot_str` - String representation of the slot for output
519    /// * `mapping_slots` - Tracked mapping slot accesses for key resolution
520    fn handle_mapping(
521        &self,
522        storage: &Storage,
523        storage_type: &StorageType,
524        slot: &B256,
525        slot_str: &str,
526        mapping_slots: &MappingSlots,
527    ) -> Option<SlotInfo> {
528        trace!(
529            "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}",
530            storage.slot,
531            slot,
532            mapping_slots.keys.contains_key(slot),
533            mapping_slots.parent_slots.contains_key(slot)
534        );
535
536        // Verify it's actually a mapping type
537        if storage_type.encoding != ENCODING_MAPPING {
538            return None;
539        }
540
541        // Check if this slot is a known mapping entry
542        if !mapping_slots.keys.contains_key(slot) {
543            return None;
544        }
545
546        // Convert storage.slot to B256 for comparison
547        let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?);
548
549        // Walk up the parent chain to collect keys and validate the base slot
550        let mut current_slot = *slot;
551        let mut keys_to_decode = Vec::new();
552        let mut found_base = false;
553
554        while let Some((key, parent)) =
555            mapping_slots.keys.get(&current_slot).zip(mapping_slots.parent_slots.get(&current_slot))
556        {
557            keys_to_decode.push(*key);
558
559            // Check if the parent is our base storage slot
560            if *parent == storage_slot_b256 {
561                found_base = true;
562                break;
563            }
564
565            // Move up to the parent for the next iteration
566            current_slot = *parent;
567        }
568
569        if !found_base {
570            trace!("Mapping slot {} does not match any parent in chain", storage.slot);
571            return None;
572        }
573
574        // Resolve the mapping type to get all key types and the final value type
575        let (key_types, value_type_label, full_type_label) =
576            self.resolve_mapping_type(&storage.storage_type)?;
577
578        // Reverse keys to process from outermost to innermost
579        keys_to_decode.reverse();
580
581        // Build the label with decoded keys and collect decoded key values
582        let mut label = storage.label.clone();
583        let mut decoded_keys = Vec::new();
584
585        // Decode each key using the corresponding type
586        for (i, key) in keys_to_decode.iter().enumerate() {
587            if let Some(key_type_label) = key_types.get(i)
588                && let Ok(sol_type) = DynSolType::parse(key_type_label)
589                && let Ok(decoded) = sol_type.abi_decode(&key.0)
590            {
591                let decoded_key_str = format_token_raw(&decoded);
592                decoded_keys.push(decoded_key_str.clone());
593                label = format!("{label}[{decoded_key_str}]");
594            } else {
595                let hex_key = hex::encode_prefixed(key.0);
596                decoded_keys.push(hex_key.clone());
597                label = format!("{label}[{hex_key}]");
598            }
599        }
600
601        // Parse the final value type for decoding
602        let dyn_sol_type = DynSolType::parse(&value_type_label).unwrap_or(DynSolType::Bytes);
603
604        Some(SlotInfo {
605            label,
606            slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type },
607            offset: storage.offset,
608            slot: slot_str.to_string(),
609            members: None,
610            decoded: None,
611            keys: Some(decoded_keys),
612        })
613    }
614
615    fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec<String>, String, String)> {
616        let storage_type = self.storage_layout.types.get(type_ref)?;
617
618        if storage_type.encoding != ENCODING_MAPPING {
619            // Not a mapping, return the type as-is
620            return Some((vec![], storage_type.label.clone(), storage_type.label.clone()));
621        }
622
623        // Get key and value type references
624        let key_type_ref = storage_type.key.as_ref()?;
625        let value_type_ref = storage_type.value.as_ref()?;
626
627        // Resolve the key type
628        let key_type = self.storage_layout.types.get(key_type_ref)?;
629        let mut key_types = vec![key_type.label.clone()];
630
631        // Check if the value is another mapping (nested case)
632        if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
633            if value_storage_type.encoding == ENCODING_MAPPING {
634                // Recursively resolve the nested mapping
635                let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
636                key_types.extend(nested_keys);
637                return Some((key_types, final_value, storage_type.label.clone()));
638            } else {
639                // Value is not a mapping, we're done
640                return Some((
641                    key_types,
642                    value_storage_type.label.clone(),
643                    storage_type.label.clone(),
644                ));
645            }
646        }
647
648        None
649    }
650}
651
652/// Returns the base indices for array types, e.g. "\[0\]\[0\]" for 2D arrays.
653fn get_array_base_indices(dyn_type: &DynSolType) -> String {
654    match dyn_type {
655        DynSolType::FixedArray(inner, _) => {
656            if let DynSolType::FixedArray(_, _) = inner.as_ref() {
657                // Nested array (2D or higher)
658                format!("[0]{}", get_array_base_indices(inner))
659            } else {
660                // Simple 1D array
661                "[0]".to_string()
662            }
663        }
664        _ => String::new(),
665    }
666}
667
668/// Checks if a given type label represents a struct type.
669pub fn is_struct(s: &str) -> bool {
670    s.starts_with("struct ")
671}