1use 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
15pub const ENCODING_INPLACE: &str = "inplace";
17pub const ENCODING_MAPPING: &str = "mapping";
19pub const ENCODING_BYTES: &str = "bytes";
22pub const ENCODING_DYN_ARRAY: &str = "dynamic_array";
24
25#[derive(Clone, Serialize, Debug)]
27pub struct SlotInfo {
28 pub label: String,
36 #[serde(rename = "type", serialize_with = "serialize_slot_type")]
38 pub slot_type: StorageTypeInfo,
39 pub offset: i64,
41 pub slot: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
47 pub members: Option<Vec<Self>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub decoded: Option<DecodedSlotValues>,
51 #[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#[derive(Clone, Debug)]
66pub struct StorageTypeInfo {
67 pub label: String,
70 pub dyn_sol_type: DynSolType,
72}
73
74impl SlotInfo {
75 pub fn decode(&self, value: B256) -> Option<DynSolValue> {
80 let mut actual_type = &self.slot_type.dyn_sol_type;
82 while let DynSolType::FixedArray(elem_type, _) = actual_type {
84 actual_type = elem_type.as_ref();
85 }
86
87 match actual_type {
89 DynSolType::Bytes | DynSolType::String => {
90 let length_byte = value.0[31];
94
95 if length_byte & 1 == 0 {
96 let length = ((length_byte >> 1) as usize).min(31);
98 let data = if length == 0 { Vec::new() } else { value.0[0..length].to_vec() };
100
101 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 None
117 }
118 }
119 _ => {
120 actual_type.abi_decode(&value.0).ok()
122 }
123 }
124 }
125
126 pub const fn is_bytes_or_string(&self) -> bool {
128 matches!(self.slot_type.dyn_sol_type, DynSolType::Bytes | DynSolType::String)
129 }
130
131 pub fn decode_bytes_or_string(
134 &mut self,
135 base_slot: &B256,
136 storage_values: &B256Map<B256>,
137 ) -> Option<DynSolValue> {
138 if !self.is_bytes_or_string() {
140 return None;
141 }
142
143 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 pub fn decode_bytes_or_string_values(
160 &mut self,
161 base_slot: &B256,
162 storage_accesses: &BTreeMap<B256, (B256, B256)>,
163 ) {
164 if !self.is_bytes_or_string() {
166 return;
167 }
168
169 if let Some((prev_base_value, new_base_value)) = storage_accesses.get(base_slot) {
171 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 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 self.decode(base_value)
184 }
185 };
186
187 let prev_decoded = decode_value(*prev_base_value, false);
189
190 let new_decoded = decode_value(*new_base_value, true);
192
193 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 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 if let Some(base_value) = storage_values.get(base_slot) {
215 let length_byte = base_value.0[31];
216
217 if length_byte & 1 == 1 {
219 let length: U256 = U256::from_be_bytes(base_value.0) >> 1;
221 let byte_len = length.try_into().unwrap_or(usize::MAX);
222 let num_slots = byte_len.div_ceil(32).min(256);
223 let data_start = U256::from_be_bytes(keccak256(base_slot.0).0);
224
225 let mut members = Vec::new();
226 let mut full_data = Vec::with_capacity(num_slots * 32);
227
228 for i in 0..num_slots {
229 let data_slot = B256::from(data_start + U256::from(i));
230 let data_slot_u256 = data_start + U256::from(i);
231
232 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 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 if !members.is_empty() {
258 self.members = Some(members);
259 }
260
261 return Some(full_data);
262 }
263 }
264
265 None
266 }
267
268 pub fn decode_values(&mut self, previous_value: B256, new_value: B256) {
271 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, };
282
283 let mut prev_bytes = [0u8; 32];
285 let mut new_bytes = [0u8; 32];
286
287 if offset + size <= 32 {
288 let byte_start = 32 - offset - size;
295 prev_bytes[32 - size..]
296 .copy_from_slice(&previous_value.0[byte_start..byte_start + size]);
297 new_bytes[32 - size..]
298 .copy_from_slice(&new_value.0[byte_start..byte_start + size]);
299 }
300
301 if let (Ok(prev_val), Ok(new_val)) = (
303 member.slot_type.dyn_sol_type.abi_decode(&prev_bytes),
304 member.slot_type.dyn_sol_type.abi_decode(&new_bytes),
305 ) {
306 member.decoded =
307 Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val });
308 }
309 }
310 } else {
312 if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) {
316 self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
317 }
318 }
319 }
320}
321
322fn serialize_slot_type<S>(info: &StorageTypeInfo, serializer: S) -> Result<S::Ok, S::Error>
324where
325 S: serde::Serializer,
326{
327 serializer.serialize_str(&info.label)
328}
329
330fn serialize_mapping_keys<S>(keys: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
332where
333 S: serde::Serializer,
334{
335 use serde::ser::SerializeMap;
336
337 if let Some(keys) = keys {
338 let len = if keys.is_empty() { 0 } else { 1 };
339 let mut map = serializer.serialize_map(Some(len))?;
340 if keys.len() == 1 {
341 map.serialize_entry("key", &keys[0])?;
342 } else if keys.len() > 1 {
343 map.serialize_entry("keys", keys)?;
344 }
345 map.end()
346 } else {
347 serializer.serialize_none()
348 }
349}
350
351#[derive(Clone, Debug)]
353pub struct DecodedSlotValues {
354 pub previous_value: DynSolValue,
356 pub new_value: DynSolValue,
358}
359
360impl Serialize for DecodedSlotValues {
361 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
362 where
363 S: serde::Serializer,
364 {
365 use serde::ser::SerializeStruct;
366
367 let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?;
368 state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?;
369 state.serialize_field("newValue", &format_token_raw(&self.new_value))?;
370 state.end()
371 }
372}
373
374#[derive(Clone)]
376pub struct SlotIdentifier {
377 storage_layout: Arc<StorageLayout>,
378 parsed_types: BTreeMap<String, Option<DynSolType>>,
379}
380
381impl SlotIdentifier {
382 pub fn new(storage_layout: Arc<StorageLayout>) -> Self {
384 let parsed_types = storage_layout
385 .types
386 .iter()
387 .map(|(id, storage_type)| (id.clone(), parse_sol_type(&storage_type.label)))
388 .collect();
389 Self { storage_layout, parsed_types }
390 }
391
392 fn parsed_type(&self, storage_type: &str) -> Option<&DynSolType> {
393 self.parsed_types.get(storage_type).and_then(Option::as_ref)
394 }
395
396 pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
400 trace!(?slot, "identifying slot");
401 let slot_u256 = U256::from_be_bytes(slot.0);
402 let slot_str = slot_u256.to_string();
403
404 for storage in &self.storage_layout.storage {
405 let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
406 let dyn_type = self.parsed_type(&storage.storage_type);
407
408 if storage.slot == slot_str
411 && let Some(parsed_type) = dyn_type.cloned()
412 {
413 let label = if let DynSolType::FixedArray(_, _) = &parsed_type {
415 format!("{}{}", storage.label, get_array_base_indices(&parsed_type))
416 } else {
417 storage.label.clone()
418 };
419
420 return Some(SlotInfo {
421 label,
422 slot_type: StorageTypeInfo {
423 label: storage_type.label.clone(),
424 dyn_sol_type: parsed_type,
425 },
426 offset: storage.offset,
427 slot: storage.slot.clone(),
428 members: None,
429 decoded: None,
430 keys: None,
431 });
432 }
433
434 if storage_type.encoding == ENCODING_INPLACE {
436 let array_start_slot = U256::from_str(&storage.slot).ok()?;
439
440 if let Some(parsed_type) = dyn_type
441 && let DynSolType::FixedArray(_, _) = parsed_type
442 && let Some(slot_info) = self.handle_array_slot(
443 storage,
444 storage_type,
445 parsed_type,
446 slot_u256,
447 array_start_slot,
448 &slot_str,
449 )
450 {
451 return Some(slot_info);
452 }
453
454 if is_struct(&storage_type.label) {
456 let struct_start_slot = U256::from_str(&storage.slot).ok()?;
457 if let Some(slot_info) = self.handle_struct(
458 &storage.label,
459 storage_type,
460 slot_u256,
461 struct_start_slot,
462 storage.offset,
463 &slot_str,
464 0,
465 ) {
466 return Some(slot_info);
467 }
468 }
469 } else if storage_type.encoding == ENCODING_MAPPING
470 && let Some(mapping_slots) = mapping_slots
471 && let Some(info) =
472 self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots)
473 {
474 return Some(info);
475 }
476 }
477
478 None
479 }
480
481 pub fn identify_bytes_or_string(
488 &self,
489 slot: &B256,
490 storage_values: &B256Map<B256>,
491 ) -> Option<SlotInfo> {
492 let slot_u256 = U256::from_be_bytes(slot.0);
493 let slot_str = slot_u256.to_string();
494
495 for storage in &self.storage_layout.storage {
497 if let Some(storage_type) = self.storage_layout.types.get(&storage.storage_type)
498 && storage_type.encoding == ENCODING_BYTES
499 {
500 let Some(base_slot) = U256::from_str(&storage.slot).map(B256::from).ok() else {
501 continue;
502 };
503 if let Some(base_value) = storage_values.get(&base_slot)
505 && let Some(info) = self.handle_bytes_string(
506 storage,
507 storage_type,
508 slot_u256,
509 &slot_str,
510 base_value,
511 )
512 {
513 return Some(info);
514 }
515 }
516 }
517
518 None
519 }
520
521 fn handle_array_slot(
530 &self,
531 storage: &Storage,
532 storage_type: &StorageType,
533 parsed_type: &DynSolType,
534 slot: U256,
535 array_start_slot: U256,
536 slot_str: &str,
537 ) -> Option<SlotInfo> {
538 let total_bytes = storage_type.number_of_bytes.parse::<u64>().ok()?;
540 let total_slots = total_bytes.div_ceil(32);
541
542 if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) {
543 let index = (slot - array_start_slot).to::<u64>();
544 let label = match parsed_type {
546 DynSolType::FixedArray(inner, _) => {
547 if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() {
548 let row = index / (*inner_size as u64);
550 let col = index % (*inner_size as u64);
551 format!("{}[{row}][{col}]", storage.label)
552 } else {
553 format!("{}[{index}]", storage.label)
555 }
556 }
557 _ => storage.label.clone(),
558 };
559
560 return Some(SlotInfo {
561 label,
562 slot_type: StorageTypeInfo {
563 label: storage_type.label.clone(),
564 dyn_sol_type: parsed_type.clone(),
565 },
566 offset: 0,
567 slot: slot_str.to_string(),
568 members: None,
569 decoded: None,
570 keys: None,
571 });
572 }
573
574 None
575 }
576
577 #[allow(clippy::too_many_arguments)]
591 fn handle_struct(
592 &self,
593 base_label: &str,
594 storage_type: &StorageType,
595 target_slot: U256,
596 struct_start_slot: U256,
597 offset: i64,
598 slot_str: &str,
599 depth: usize,
600 ) -> Option<SlotInfo> {
601 const MAX_DEPTH: usize = 10;
603 if depth > MAX_DEPTH {
604 return None;
605 }
606
607 let members = storage_type
608 .other
609 .get("members")
610 .and_then(|v| serde_json::from_value::<Vec<Storage>>(v.clone()).ok())?;
611
612 if struct_start_slot == target_slot
614 && let Some(first_member) = members.iter().find(|m| m.slot == "0")
616 {
617 let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?;
618
619 let is_single_slot = members.iter().all(|m| m.slot == "0");
621
622 if is_single_slot {
623 let mut member_infos = Vec::new();
625 for member in &members {
626 if let Some(member_type_info) =
627 self.storage_layout.types.get(&member.storage_type)
628 && let Some(member_type) = self.parsed_type(&member.storage_type).cloned()
629 {
630 member_infos.push(SlotInfo {
631 label: member.label.clone(),
632 slot_type: StorageTypeInfo {
633 label: member_type_info.label.clone(),
634 dyn_sol_type: member_type,
635 },
636 offset: member.offset,
637 slot: slot_str.to_string(),
638 members: None,
639 decoded: None,
640 keys: None,
641 });
642 }
643 }
644
645 let struct_name =
647 storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label);
648 let prop_names: Vec<String> = members.iter().map(|m| m.label.clone()).collect();
649 let member_types: Vec<DynSolType> =
650 member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect();
651
652 let parsed_type = DynSolType::CustomStruct {
653 name: struct_name.to_string(),
654 prop_names,
655 tuple: member_types,
656 };
657
658 return Some(SlotInfo {
659 label: base_label.to_string(),
660 slot_type: StorageTypeInfo {
661 label: storage_type.label.clone(),
662 dyn_sol_type: parsed_type,
663 },
664 offset,
665 slot: slot_str.to_string(),
666 decoded: None,
667 members: if member_infos.is_empty() { None } else { Some(member_infos) },
668 keys: None,
669 });
670 }
671
672 let member_label = format!("{}.{}", base_label, first_member.label);
674
675 if is_struct(&member_type_info.label) {
677 return self.handle_struct(
678 &member_label,
679 member_type_info,
680 target_slot,
681 struct_start_slot,
682 first_member.offset,
683 slot_str,
684 depth + 1,
685 );
686 }
687
688 return Some(SlotInfo {
690 label: member_label,
691 slot_type: StorageTypeInfo {
692 label: member_type_info.label.clone(),
693 dyn_sol_type: self.parsed_type(&first_member.storage_type)?.clone(),
694 },
695 offset: first_member.offset,
696 slot: slot_str.to_string(),
697 decoded: None,
698 members: None,
699 keys: None,
700 });
701 }
702
703 for member in &members {
705 let member_slot_offset = U256::from_str(&member.slot).ok()?;
706 let member_slot = struct_start_slot + member_slot_offset;
707 let member_type_info = self.storage_layout.types.get(&member.storage_type)?;
708 let member_label = format!("{}.{}", base_label, member.label);
709
710 if is_struct(&member_type_info.label) {
712 let slot_info = self.handle_struct(
713 &member_label,
714 member_type_info,
715 target_slot,
716 member_slot,
717 member.offset,
718 slot_str,
719 depth + 1,
720 );
721
722 if member_slot == target_slot || slot_info.is_some() {
723 return slot_info;
724 }
725 }
726
727 if member_slot == target_slot {
728 let member_type = self.parsed_type(&member.storage_type)?.clone();
732 return Some(SlotInfo {
733 label: member_label,
734 slot_type: StorageTypeInfo {
735 label: member_type_info.label.clone(),
736 dyn_sol_type: member_type,
737 },
738 offset: member.offset,
739 slot: slot_str.to_string(),
740 members: None,
741 decoded: None,
742 keys: None,
743 });
744 }
745 }
746
747 None
748 }
749
750 fn handle_mapping(
762 &self,
763 storage: &Storage,
764 storage_type: &StorageType,
765 slot: &B256,
766 slot_str: &str,
767 mapping_slots: &MappingSlots,
768 ) -> Option<SlotInfo> {
769 trace!(
770 "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}",
771 storage.slot,
772 slot,
773 mapping_slots.keys.contains_key(slot),
774 mapping_slots.parent_slots.contains_key(slot)
775 );
776
777 if storage_type.encoding != ENCODING_MAPPING {
779 return None;
780 }
781
782 if !mapping_slots.keys.contains_key(slot) {
784 return None;
785 }
786
787 let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?);
789
790 let mut current_slot = *slot;
792 let mut keys_to_decode = Vec::new();
793 let mut found_base = false;
794
795 while let Some((key, parent)) =
796 mapping_slots.keys.get(¤t_slot).zip(mapping_slots.parent_slots.get(¤t_slot))
797 {
798 keys_to_decode.push(*key);
799
800 if *parent == storage_slot_b256 {
802 found_base = true;
803 break;
804 }
805
806 current_slot = *parent;
808 }
809
810 if !found_base {
811 trace!("Mapping slot {} does not match any parent in chain", storage.slot);
812 return None;
813 }
814
815 let (key_types, value_type_label, full_type_label) =
817 self.resolve_mapping_type(&storage.storage_type)?;
818
819 keys_to_decode.reverse();
821
822 let mut label = storage.label.clone();
824 let mut decoded_keys = Vec::new();
825
826 for (i, key) in keys_to_decode.iter().enumerate() {
828 if let Some(key_type_label) = key_types.get(i)
829 && let Some(sol_type) = parse_sol_type(key_type_label)
830 && let Ok(decoded) = sol_type.abi_decode(&key.0)
831 {
832 let decoded_key_str = format_token_raw(&decoded);
833 decoded_keys.push(decoded_key_str.clone());
834 label = format!("{label}[{decoded_key_str}]");
835 } else {
836 let hex_key = hex::encode_prefixed(key.0);
837 decoded_keys.push(hex_key.clone());
838 label = format!("{label}[{hex_key}]");
839 }
840 }
841
842 let dyn_sol_type = parse_sol_type(&value_type_label).unwrap_or(DynSolType::Bytes);
845
846 Some(SlotInfo {
847 label,
848 slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type },
849 offset: storage.offset,
850 slot: slot_str.to_string(),
851 members: None,
852 decoded: None,
853 keys: Some(decoded_keys),
854 })
855 }
856
857 fn handle_bytes_string(
873 &self,
874 storage: &Storage,
875 storage_type: &StorageType,
876 slot: U256,
877 slot_str: &str,
878 base_slot_value: &B256,
879 ) -> Option<SlotInfo> {
880 if storage_type.encoding != ENCODING_BYTES {
882 return None;
883 }
884
885 let base_slot = U256::from_str(&storage.slot).ok()?;
887 if slot == base_slot {
888 let dyn_type = if storage_type.label == "string" {
890 DynSolType::String
891 } else if storage_type.label == "bytes" {
892 DynSolType::Bytes
893 } else {
894 return None;
895 };
896
897 return Some(SlotInfo {
898 label: storage.label.clone(),
899 slot_type: StorageTypeInfo {
900 label: storage_type.label.clone(),
901 dyn_sol_type: dyn_type,
902 },
903 offset: storage.offset,
904 slot: slot_str.to_string(),
905 members: None,
906 decoded: None,
907 keys: None,
908 });
909 }
910
911 let data_start =
914 U256::from_be_bytes(alloy_primitives::keccak256(base_slot.to_be_bytes::<32>()).0);
915
916 let length_byte = base_slot_value.0[31];
919 if length_byte & 1 == 1 {
920 let length = U256::from_be_bytes(base_slot_value.0) >> 1;
922 let num_slots = (length + U256::from(31)) / U256::from(32);
924
925 if slot >= data_start && slot < data_start + num_slots {
927 let slot_index = (slot - data_start).try_into().unwrap_or(usize::MAX);
928
929 return Some(SlotInfo {
930 label: format!("{}[{}]", storage.label, slot_index),
931 slot_type: StorageTypeInfo {
932 label: storage_type.label.clone(),
933 dyn_sol_type: DynSolType::FixedBytes(32),
935 },
936 offset: 0,
937 slot: slot_str.to_string(),
938 members: None,
939 decoded: None,
940 keys: None,
941 });
942 }
943 }
944
945 None
946 }
947
948 fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec<String>, String, String)> {
949 let storage_type = self.storage_layout.types.get(type_ref)?;
950
951 if storage_type.encoding != ENCODING_MAPPING {
952 return Some((vec![], storage_type.label.clone(), storage_type.label.clone()));
954 }
955
956 let key_type_ref = storage_type.key.as_ref()?;
958 let value_type_ref = storage_type.value.as_ref()?;
959
960 let key_type = self.storage_layout.types.get(key_type_ref)?;
962 let mut key_types = vec![key_type.label.clone()];
963
964 if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
966 if value_storage_type.encoding == ENCODING_MAPPING {
967 let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
969 key_types.extend(nested_keys);
970 return Some((key_types, final_value, storage_type.label.clone()));
971 }
972 return Some((key_types, value_storage_type.label.clone(), storage_type.label.clone()));
974 }
975
976 None
977 }
978}
979
980fn get_array_base_indices(dyn_type: &DynSolType) -> String {
982 match dyn_type {
983 DynSolType::FixedArray(inner, _) => {
984 if let DynSolType::FixedArray(_, _) = inner.as_ref() {
985 format!("[0]{}", get_array_base_indices(inner))
987 } else {
988 "[0]".to_string()
990 }
991 }
992 _ => String::new(),
993 }
994}
995
996fn parse_sol_type(label: &str) -> Option<DynSolType> {
1002 let scalar = if label.starts_with("contract ") {
1003 "address"
1004 } else if label.starts_with("enum ") {
1005 "uint8"
1006 } else {
1007 return DynSolType::parse(label).ok();
1008 };
1009 let suffix = label.find('[').map_or("", |index| &label[index..]);
1010 DynSolType::parse(&format!("{scalar}{suffix}")).ok()
1011}
1012
1013pub fn is_struct(s: &str) -> bool {
1015 s.starts_with("struct ")
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 #[test]
1023 fn preserves_contract_and_enum_array_dimensions() {
1024 for (label, expected) in [
1025 ("contract IERC20", "address"),
1026 ("contract IERC20[]", "address[]"),
1027 ("contract IERC20[4][]", "address[4][]"),
1028 ("enum Example.Status", "uint8"),
1029 ("enum Example.Status[4]", "uint8[4]"),
1030 ("enum Example.Status[][4]", "uint8[][4]"),
1031 ] {
1032 assert_eq!(parse_sol_type(label), DynSolType::parse(expected).ok(), "{label}");
1033 }
1034 }
1035}