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