Skip to main content

soldb_ethdebug/
storage_layout.rs

1//! solc's storage layout: where every state variable lives and how it is encoded.
2//!
3//! `solc --storage-layout` (any release since 0.5.13, the legacy pipeline included) emits,
4//! per contract, the slot, byte offset, and type of each state variable and a table of
5//! the types with their encodings. That is compiler-issued fact, not inference, which is
6//! why a debugger may read `counter`, `balances[0xabc]`, `config.owner`, or `items[2]`
7//! from a storage snapshot without ETHDebug variable information: the slot arithmetic for
8//! mappings, arrays, and structs is the language's own, fixed rule.
9//!
10//! [`StorageLayout::resolve`] turns a path into a slot and byte offset;
11//! [`StorageLayout::decode`] turns the words at those slots into a value, reading each
12//! word through a callback so the caller decides what "the storage at this step" means.
13//! Nothing here reads a chain.
14
15use std::collections::BTreeMap;
16
17use serde_json::Value;
18
19use soldb_core::{SoldbError, SoldbResult};
20
21use crate::abi::keccak256;
22
23/// A 256-bit storage word or slot number, big-endian.
24pub type Word = [u8; 32];
25
26/// The layout of one contract's storage as solc emitted it.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct StorageLayout {
29    /// State variables in declaration order.
30    pub variables: Vec<StorageVariable>,
31    /// Types by solc's identifier, such as `t_uint256` or `t_mapping(t_address,t_uint256)`.
32    pub types: BTreeMap<String, StorageType>,
33    /// The layout exactly as the compiler wrote it, for handing to a client that decodes
34    /// slots itself.
35    pub source: Value,
36    /// The variants of each enum, in declaration order, keyed by the name a type label
37    /// spells (`Shop.Color`) and by its bare name (`Color`). The layout itself only
38    /// numbers enum values; whoever has the sources fills this in, and a value then
39    /// decodes as `Color.Blue` rather than `2`.
40    pub enum_variants: BTreeMap<String, Vec<String>>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct StorageVariable {
45    pub label: String,
46    pub slot: Word,
47    /// Byte offset within the slot, counted from the least significant byte.
48    pub offset: u64,
49    pub type_id: String,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum StorageEncoding {
54    /// Stored in place, packed from the least significant byte of its slot.
55    Inplace,
56    /// Values live at `keccak256(key . slot)`.
57    Mapping,
58    /// The length at the slot, elements from `keccak256(slot)`.
59    DynamicArray,
60    /// A `bytes` or `string`: short values in the slot, long ones from `keccak256(slot)`.
61    Bytes,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct StorageType {
66    /// The Solidity type as written, such as `uint256` or `mapping(address => uint256)`.
67    pub label: String,
68    pub encoding: StorageEncoding,
69    pub number_of_bytes: u64,
70    /// A mapping's key type.
71    pub key: Option<String>,
72    /// A mapping's value type.
73    pub value: Option<String>,
74    /// An array's element type.
75    pub base: Option<String>,
76    /// A struct's members.
77    pub members: Vec<StorageMember>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct StorageMember {
82    pub label: String,
83    /// Slot relative to the struct's first slot.
84    pub slot: u64,
85    pub offset: u64,
86    pub type_id: String,
87}
88
89/// A resolved place in storage: what a path names.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct StorageRef {
92    /// The path as resolved, such as `balances[0xabc]`.
93    pub path: String,
94    pub slot: Word,
95    pub offset: u64,
96    pub type_id: String,
97}
98
99/// A value read out of storage.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct DecodedStorage {
102    /// The value as the debugger shows it.
103    pub display: String,
104    /// The word the value came from, for value types, as `0x`-prefixed hex.
105    pub raw: Option<String>,
106}
107
108impl StorageLayout {
109    /// Parses the `<Contract>_storage.json` solc writes, or the `storageLayout` object of
110    /// standard JSON output.
111    pub fn parse(value: &Value) -> SoldbResult<Self> {
112        let entries = value
113            .get("storage")
114            .and_then(Value::as_array)
115            .ok_or_else(|| {
116                SoldbError::Message("storage layout has no `storage` array".to_owned())
117            })?;
118        let variables = entries
119            .iter()
120            .map(|entry| {
121                Ok(StorageVariable {
122                    label: text(entry, "label")?.to_owned(),
123                    slot: parse_word(text(entry, "slot")?)?,
124                    offset: number(entry, "offset")?,
125                    type_id: text(entry, "type")?.to_owned(),
126                })
127            })
128            .collect::<SoldbResult<Vec<_>>>()?;
129        let types = value
130            .get("types")
131            .and_then(Value::as_object)
132            .map(|types| {
133                types
134                    .iter()
135                    .map(|(id, ty)| Ok((id.clone(), parse_type(ty)?)))
136                    .collect::<SoldbResult<BTreeMap<_, _>>>()
137            })
138            .transpose()?
139            .unwrap_or_default();
140        Ok(Self {
141            variables,
142            types,
143            source: value.clone(),
144            enum_variants: BTreeMap::new(),
145        })
146    }
147
148    #[must_use]
149    pub fn variable(&self, label: &str) -> Option<&StorageVariable> {
150        self.variables
151            .iter()
152            .find(|variable| variable.label == label)
153    }
154
155    #[must_use]
156    pub fn type_of(&self, type_id: &str) -> Option<&StorageType> {
157        self.types.get(type_id)
158    }
159
160    /// The type of a variable, when the layout describes it.
161    pub fn variable_type(&self, variable: &StorageVariable) -> SoldbResult<&StorageType> {
162        self.type_of(&variable.type_id).ok_or_else(|| {
163            SoldbError::Message(format!(
164                "storage layout does not describe type `{}` of `{}`",
165                variable.type_id, variable.label
166            ))
167        })
168    }
169
170    /// Resolves a path to a place in storage: a variable, a mapping entry (`m[key]`), an
171    /// array element (`a[i]`), a struct member (`s.member`), or any chain of those.
172    pub fn resolve(&self, path: &str) -> SoldbResult<StorageRef> {
173        let mut segments = parse_path(path)?.into_iter();
174        let Some(PathSegment::Name(name)) = segments.next() else {
175            return Err(SoldbError::Message(format!(
176                "`{path}` does not start with a state variable name"
177            )));
178        };
179        let variable = self.variable(&name).ok_or_else(|| {
180            SoldbError::Message(format!(
181                "no state variable named `{name}` in the storage layout"
182            ))
183        })?;
184        let start = StorageRef {
185            path: name.clone(),
186            slot: variable.slot,
187            offset: variable.offset,
188            type_id: variable.type_id.clone(),
189        };
190        self.walk(start, segments)
191    }
192
193    /// Follows `segments` from `start`: a mapping entry (`[key]`), an array element
194    /// (`[i]`), a struct member (`.member`), or any chain of those. This is how a path
195    /// continues from a place that is not a state variable, such as a storage pointer
196    /// held by a local variable.
197    pub fn walk(
198        &self,
199        start: StorageRef,
200        segments: impl IntoIterator<Item = PathSegment>,
201    ) -> SoldbResult<StorageRef> {
202        let mut current = start;
203        for segment in segments {
204            current = self.step(current, segment)?;
205        }
206        Ok(current)
207    }
208
209    /// `Color.Blue` for an enum value whose variants are known; see
210    /// [`StorageLayout::enum_variants`].
211    fn enum_display(&self, label: &str, bytes: &[u8]) -> Option<String> {
212        let name = label.strip_prefix("enum ")?;
213        let short = name.rsplit('.').next().unwrap_or(name);
214        let variants = self
215            .enum_variants
216            .get(name)
217            .or_else(|| self.enum_variants.get(short))?;
218        let significant = bytes
219            .iter()
220            .position(|byte| *byte != 0)
221            .map_or(&[][..], |first| &bytes[first..]);
222        if significant.len() > 8 {
223            return None;
224        }
225        let index = significant
226            .iter()
227            .fold(0_usize, |value, byte| (value << 8) | usize::from(*byte));
228        Some(format!("{short}.{}", variants.get(index)?))
229    }
230
231    fn step(&self, current: StorageRef, segment: PathSegment) -> SoldbResult<StorageRef> {
232        let ty = self.type_of(&current.type_id).ok_or_else(|| {
233            SoldbError::Message(format!(
234                "storage layout does not describe type `{}`",
235                current.type_id
236            ))
237        })?;
238        match segment {
239            PathSegment::Index(key) => match ty.encoding {
240                StorageEncoding::Mapping => {
241                    let key_type_id = ty.key.as_deref().ok_or_else(|| {
242                        SoldbError::Message(format!("mapping type `{}` has no key type", ty.label))
243                    })?;
244                    let key_type = self.type_of(key_type_id).ok_or_else(|| {
245                        SoldbError::Message(format!(
246                            "storage layout does not describe key type `{key_type_id}`"
247                        ))
248                    })?;
249                    let value_type_id = ty.value.clone().ok_or_else(|| {
250                        SoldbError::Message(format!(
251                            "mapping type `{}` has no value type",
252                            ty.label
253                        ))
254                    })?;
255                    let slot = mapping_slot(&current.slot, &encode_mapping_key(&key, key_type)?);
256                    Ok(StorageRef {
257                        path: format!("{}[{key}]", current.path),
258                        slot,
259                        offset: 0,
260                        type_id: value_type_id,
261                    })
262                }
263                StorageEncoding::DynamicArray | StorageEncoding::Inplace if ty.base.is_some() => {
264                    let index = parse_index(&key)?;
265                    let base_id = ty.base.clone().expect("checked above");
266                    let base = self.type_of(&base_id).ok_or_else(|| {
267                        SoldbError::Message(format!(
268                            "storage layout does not describe element type `{base_id}`"
269                        ))
270                    })?;
271                    if ty.encoding == StorageEncoding::Inplace {
272                        let length = static_array_length(&ty.label);
273                        if length.is_some_and(|length| index >= length) {
274                            return Err(SoldbError::Message(format!(
275                                "index {index} is out of range for `{}` of type `{}`",
276                                current.path, ty.label
277                            )));
278                        }
279                    }
280                    let first = match ty.encoding {
281                        StorageEncoding::DynamicArray => keccak256(&current.slot),
282                        _ => current.slot,
283                    };
284                    let (slot, offset) = element_place(&first, index, base.number_of_bytes);
285                    Ok(StorageRef {
286                        path: format!("{}[{index}]", current.path),
287                        slot,
288                        offset,
289                        type_id: base_id,
290                    })
291                }
292                _ => Err(SoldbError::Message(format!(
293                    "`{}` is a `{}`, which cannot be indexed",
294                    current.path, ty.label
295                ))),
296            },
297            PathSegment::Member(member) => {
298                if ty.members.is_empty() {
299                    return Err(SoldbError::Message(format!(
300                        "`{}` is a `{}`, which has no members",
301                        current.path, ty.label
302                    )));
303                }
304                let found = ty
305                    .members
306                    .iter()
307                    .find(|candidate| candidate.label == member)
308                    .ok_or_else(|| {
309                        SoldbError::Message(format!(
310                            "`{}` has no member `{member}`; it has {}",
311                            ty.label,
312                            ty.members
313                                .iter()
314                                .map(|member| member.label.as_str())
315                                .collect::<Vec<_>>()
316                                .join(", ")
317                        ))
318                    })?;
319                Ok(StorageRef {
320                    path: format!("{}.{member}", current.path),
321                    slot: add_word(&current.slot, found.slot),
322                    offset: found.offset,
323                    type_id: found.type_id.clone(),
324                })
325            }
326            PathSegment::Name(name) => Err(SoldbError::Message(format!(
327                "unexpected name `{name}` after `{}`",
328                current.path
329            ))),
330        }
331    }
332
333    /// Decodes the value at `reference`, reading each storage word it needs through
334    /// `read`. `Err` names the first slot whose word was not available.
335    pub fn decode(
336        &self,
337        reference: &StorageRef,
338        read: &dyn Fn(&Word) -> Option<Word>,
339    ) -> Result<DecodedStorage, Word> {
340        let Some(ty) = self.type_of(&reference.type_id) else {
341            return Ok(DecodedStorage {
342                display: format!(
343                    "<type `{}` is not in the storage layout>",
344                    reference.type_id
345                ),
346                raw: None,
347            });
348        };
349        match ty.encoding {
350            StorageEncoding::Mapping => Ok(DecodedStorage {
351                display: "<mapping; index it with [key]>".to_owned(),
352                raw: None,
353            }),
354            StorageEncoding::DynamicArray => {
355                let length = read(&reference.slot).ok_or(reference.slot)?;
356                Ok(DecodedStorage {
357                    display: format!(
358                        "<{} element(s); index it with [i]>",
359                        word_to_decimal(&length)
360                    ),
361                    raw: Some(word_hex(&length)),
362                })
363            }
364            StorageEncoding::Bytes => {
365                let head = read(&reference.slot).ok_or(reference.slot)?;
366                let bytes = if head[31] & 1 == 0 {
367                    // Short: the data sits in the slot and the last byte is twice the length.
368                    let length = usize::from(head[31] / 2);
369                    head[..length.min(31)].to_vec()
370                } else {
371                    // Long: the slot holds twice the length plus one, the data follows
372                    // keccak256(slot) word by word.
373                    let length = usize::try_from(u128::from_be_bytes(
374                        head[16..].try_into().expect("16 bytes"),
375                    ))
376                    .map(|doubled| doubled / 2)
377                    .unwrap_or(usize::MAX);
378                    let mut bytes = Vec::with_capacity(length.min(1 << 16));
379                    let mut slot = keccak256(&reference.slot);
380                    while bytes.len() < length {
381                        let word = read(&slot).ok_or(slot)?;
382                        let take = (length - bytes.len()).min(32);
383                        bytes.extend_from_slice(&word[..take]);
384                        slot = add_word(&slot, 1);
385                    }
386                    bytes
387                };
388                let display = if ty.label == "string" {
389                    match std::str::from_utf8(&bytes) {
390                        Ok(text) => format!("{text:?}"),
391                        Err(_) => format!("0x{}", hex(&bytes)),
392                    }
393                } else {
394                    format!("0x{}", hex(&bytes))
395                };
396                Ok(DecodedStorage {
397                    display,
398                    raw: Some(word_hex(&head)),
399                })
400            }
401            StorageEncoding::Inplace if !ty.members.is_empty() => {
402                let mut parts = Vec::with_capacity(ty.members.len());
403                for member in &ty.members {
404                    let inner = StorageRef {
405                        path: format!("{}.{}", reference.path, member.label),
406                        slot: add_word(&reference.slot, member.slot),
407                        offset: member.offset,
408                        type_id: member.type_id.clone(),
409                    };
410                    let value = self.decode(&inner, read)?;
411                    parts.push(format!("{}: {}", member.label, value.display));
412                }
413                Ok(DecodedStorage {
414                    display: format!("{{ {} }}", parts.join(", ")),
415                    raw: None,
416                })
417            }
418            StorageEncoding::Inplace if ty.base.is_some() => {
419                let base_id = ty.base.as_deref().expect("checked above");
420                let Some(base) = self.type_of(base_id) else {
421                    return Ok(DecodedStorage {
422                        display: format!("<element type `{base_id}` is not in the storage layout>"),
423                        raw: None,
424                    });
425                };
426                let length = static_array_length(&ty.label).unwrap_or(0);
427                let shown = length.min(8);
428                let mut parts = Vec::with_capacity(shown as usize);
429                for index in 0..shown {
430                    let (slot, offset) =
431                        element_place(&reference.slot, index, base.number_of_bytes);
432                    let inner = StorageRef {
433                        path: format!("{}[{index}]", reference.path),
434                        slot,
435                        offset,
436                        type_id: base_id.to_owned(),
437                    };
438                    parts.push(self.decode(&inner, read)?.display);
439                }
440                if length > shown {
441                    parts.push(format!("... {} more", length - shown));
442                }
443                Ok(DecodedStorage {
444                    display: format!("[{}]", parts.join(", ")),
445                    raw: None,
446                })
447            }
448            StorageEncoding::Inplace => {
449                let word = read(&reference.slot).ok_or(reference.slot)?;
450                let size = usize::try_from(ty.number_of_bytes)
451                    .unwrap_or(32)
452                    .clamp(1, 32);
453                let offset = usize::try_from(reference.offset)
454                    .unwrap_or(0)
455                    .min(32 - size);
456                let end = 32 - offset;
457                let bytes = &word[end - size..end];
458                Ok(DecodedStorage {
459                    display: self
460                        .enum_display(&ty.label, bytes)
461                        .unwrap_or_else(|| decode_value(bytes, &ty.label)),
462                    raw: Some(word_hex(&word)),
463                })
464            }
465        }
466    }
467}
468
469/// One step of a path such as `balances[0xabc].total`: the name it starts with, an index
470/// or key in brackets, or a member after a dot.
471#[derive(Debug, Clone, PartialEq, Eq)]
472pub enum PathSegment {
473    Name(String),
474    Index(String),
475    Member(String),
476}
477
478/// Splits `name`, `name[key]`, `name.member`, or any chain of those into its segments.
479pub fn parse_path(path: &str) -> SoldbResult<Vec<PathSegment>> {
480    let path = path.trim();
481    let bytes = path.as_bytes();
482    let mut segments = Vec::new();
483    let mut index = 0;
484    let identifier_end = |from: usize| {
485        let mut end = from;
486        while end < bytes.len()
487            && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'$')
488        {
489            end += 1;
490        }
491        end
492    };
493    let end = identifier_end(0);
494    if end == 0 {
495        return Err(SoldbError::Message(format!(
496            "`{path}` does not start with a state variable name"
497        )));
498    }
499    segments.push(PathSegment::Name(path[..end].to_owned()));
500    index = end.max(index);
501    while index < bytes.len() {
502        match bytes[index] {
503            b'[' => {
504                let mut depth = 1;
505                let mut close = index + 1;
506                while close < bytes.len() && depth > 0 {
507                    match bytes[close] {
508                        b'[' => depth += 1,
509                        b']' => depth -= 1,
510                        _ => {}
511                    }
512                    close += 1;
513                }
514                if depth != 0 {
515                    return Err(SoldbError::Message(format!("`{path}` has an unclosed `[`")));
516                }
517                let key = path[index + 1..close - 1].trim();
518                if key.is_empty() {
519                    return Err(SoldbError::Message(format!("`{path}` has an empty index")));
520                }
521                segments.push(PathSegment::Index(key.to_owned()));
522                index = close;
523            }
524            b'.' => {
525                let end = identifier_end(index + 1);
526                if end == index + 1 {
527                    return Err(SoldbError::Message(format!(
528                        "`{path}` has a `.` with no member name after it"
529                    )));
530                }
531                segments.push(PathSegment::Member(path[index + 1..end].to_owned()));
532                index = end;
533            }
534            _ => {
535                return Err(SoldbError::Message(format!(
536                    "unexpected `{}` in `{path}`; use `name`, `name[key]`, or `name.member`",
537                    &path[index..index + 1]
538                )))
539            }
540        }
541    }
542    Ok(segments)
543}
544
545fn parse_type(value: &Value) -> SoldbResult<StorageType> {
546    let encoding = match text(value, "encoding")? {
547        "inplace" => StorageEncoding::Inplace,
548        "mapping" => StorageEncoding::Mapping,
549        "dynamic_array" => StorageEncoding::DynamicArray,
550        "bytes" => StorageEncoding::Bytes,
551        other => {
552            return Err(SoldbError::Message(format!(
553                "unknown storage encoding `{other}`"
554            )))
555        }
556    };
557    let members = value
558        .get("members")
559        .and_then(Value::as_array)
560        .map(|members| {
561            members
562                .iter()
563                .map(|member| {
564                    Ok(StorageMember {
565                        label: text(member, "label")?.to_owned(),
566                        slot: text(member, "slot")?.parse::<u64>().map_err(|error| {
567                            SoldbError::Message(format!("invalid member slot: {error}"))
568                        })?,
569                        offset: number(member, "offset")?,
570                        type_id: text(member, "type")?.to_owned(),
571                    })
572                })
573                .collect::<SoldbResult<Vec<_>>>()
574        })
575        .transpose()?
576        .unwrap_or_default();
577    Ok(StorageType {
578        label: text(value, "label")?.to_owned(),
579        encoding,
580        number_of_bytes: text(value, "numberOfBytes")?
581            .parse::<u64>()
582            .map_err(|error| SoldbError::Message(format!("invalid numberOfBytes: {error}")))?,
583        key: value.get("key").and_then(Value::as_str).map(str::to_owned),
584        value: value
585            .get("value")
586            .and_then(Value::as_str)
587            .map(str::to_owned),
588        base: value.get("base").and_then(Value::as_str).map(str::to_owned),
589        members,
590    })
591}
592
593fn text<'a>(value: &'a Value, key: &str) -> SoldbResult<&'a str> {
594    value
595        .get(key)
596        .and_then(Value::as_str)
597        .ok_or_else(|| SoldbError::Message(format!("storage layout entry has no `{key}`")))
598}
599
600fn number(value: &Value, key: &str) -> SoldbResult<u64> {
601    match value.get(key) {
602        Some(Value::Number(number)) => number
603            .as_u64()
604            .ok_or_else(|| SoldbError::Message(format!("`{key}` is not a whole number"))),
605        Some(Value::String(text)) => text
606            .parse::<u64>()
607            .map_err(|error| SoldbError::Message(format!("invalid `{key}`: {error}"))),
608        _ => Err(SoldbError::Message(format!(
609            "storage layout entry has no `{key}`"
610        ))),
611    }
612}
613
614/// Parses a decimal or `0x` hex number into a word.
615pub fn parse_word(text: &str) -> SoldbResult<Word> {
616    let text = text.trim();
617    let mut word = [0_u8; 32];
618    if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
619        if hex.is_empty() || hex.len() > 64 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
620            return Err(SoldbError::Message(format!(
621                "`{text}` is not a 256-bit hex number"
622            )));
623        }
624        let padded = format!("{hex:0>64}");
625        for (index, chunk) in padded.as_bytes().chunks(2).enumerate() {
626            word[index] = u8::from_str_radix(std::str::from_utf8(chunk).expect("ascii"), 16)
627                .expect("hex digits");
628        }
629        return Ok(word);
630    }
631    if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
632        return Err(SoldbError::Message(format!(
633            "`{text}` is not a decimal or `0x` hex number"
634        )));
635    }
636    for digit in text.bytes() {
637        // word = word * 10 + digit, big-endian with a carry.
638        let mut carry = u32::from(digit - b'0');
639        for byte in word.iter_mut().rev() {
640            let value = u32::from(*byte) * 10 + carry;
641            *byte = (value & 0xff) as u8;
642            carry = value >> 8;
643        }
644        if carry != 0 {
645            return Err(SoldbError::Message(format!(
646                "`{text}` does not fit in 256 bits"
647            )));
648        }
649    }
650    Ok(word)
651}
652
653fn parse_index(text: &str) -> SoldbResult<u64> {
654    let word = parse_word(text)?;
655    if word[..24].iter().any(|byte| *byte != 0) {
656        return Err(SoldbError::Message(format!("index `{text}` is too large")));
657    }
658    Ok(u64::from_be_bytes(word[24..].try_into().expect("8 bytes")))
659}
660
661/// The word a mapping key hashes as: value types padded to a word, `string` and `bytes`
662/// as their raw bytes.
663fn encode_mapping_key(key: &str, key_type: &StorageType) -> SoldbResult<Vec<u8>> {
664    let label = key_type.label.as_str();
665    if label == "string" {
666        let text = key.trim().trim_matches('"');
667        return Ok(text.as_bytes().to_vec());
668    }
669    if label == "bytes" {
670        return hex_bytes(key.trim());
671    }
672    if label == "bool" {
673        return match key.trim() {
674            "true" | "1" => Ok(pad_left(&[1])),
675            "false" | "0" => Ok([0_u8; 32].to_vec()),
676            other => Err(SoldbError::Message(format!("`{other}` is not a bool key"))),
677        };
678    }
679    if let Some(width) = label
680        .strip_prefix("bytes")
681        .and_then(|width| width.parse::<usize>().ok())
682    {
683        let bytes = hex_bytes(key.trim())?;
684        if bytes.len() > width {
685            return Err(SoldbError::Message(format!(
686                "`{key}` is longer than {width} bytes"
687            )));
688        }
689        let mut word = [0_u8; 32];
690        word[..bytes.len()].copy_from_slice(&bytes);
691        return Ok(word.to_vec());
692    }
693    if let Some(rest) = label.strip_prefix("int") {
694        if rest.chars().all(|character| character.is_ascii_digit()) {
695            return Ok(parse_signed_word(key.trim())?.to_vec());
696        }
697    }
698    // Addresses, contracts, enums, and unsigned integers are all a number padded left.
699    Ok(parse_word(key.trim())?.to_vec())
700}
701
702fn parse_signed_word(text: &str) -> SoldbResult<Word> {
703    if let Some(magnitude) = text.strip_prefix('-') {
704        let word = parse_word(magnitude)?;
705        // Two's complement: invert and add one.
706        let mut negated = [0_u8; 32];
707        let mut carry = 1_u16;
708        for (index, byte) in word.iter().enumerate().rev() {
709            let value = u16::from(!byte) + carry;
710            negated[index] = (value & 0xff) as u8;
711            carry = value >> 8;
712        }
713        return Ok(negated);
714    }
715    parse_word(text)
716}
717
718fn pad_left(bytes: &[u8]) -> Vec<u8> {
719    let mut word = [0_u8; 32];
720    word[32 - bytes.len()..].copy_from_slice(bytes);
721    word.to_vec()
722}
723
724fn hex_bytes(text: &str) -> SoldbResult<Vec<u8>> {
725    let hex = text
726        .strip_prefix("0x")
727        .or_else(|| text.strip_prefix("0X"))
728        .unwrap_or(text);
729    if !hex.len().is_multiple_of(2) || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
730        return Err(SoldbError::Message(format!("`{text}` is not hex bytes")));
731    }
732    Ok(hex
733        .as_bytes()
734        .chunks(2)
735        .map(|chunk| {
736            u8::from_str_radix(std::str::from_utf8(chunk).expect("ascii"), 16).expect("hex")
737        })
738        .collect())
739}
740
741/// `keccak256(key . slot)`, the slot of a mapping entry.
742#[must_use]
743pub fn mapping_slot(slot: &Word, key: &[u8]) -> Word {
744    let mut input = Vec::with_capacity(key.len() + 32);
745    input.extend_from_slice(key);
746    input.extend_from_slice(slot);
747    keccak256(&input)
748}
749
750/// Where element `index` of an array whose elements take `element_bytes` sits, counting
751/// from `first`: small elements pack several to a slot, never straddling one; larger
752/// elements take whole slots.
753#[must_use]
754pub fn element_place(first: &Word, index: u64, element_bytes: u64) -> (Word, u64) {
755    let element_bytes = element_bytes.max(1);
756    if element_bytes < 32 {
757        let per_slot = 32 / element_bytes;
758        (
759            add_word(first, index / per_slot),
760            (index % per_slot) * element_bytes,
761        )
762    } else {
763        (add_word(first, index * element_bytes.div_ceil(32)), 0)
764    }
765}
766
767/// `word + addend`, wrapping at 256 bits like the EVM does.
768#[must_use]
769pub fn add_word(word: &Word, addend: u64) -> Word {
770    let mut result = *word;
771    let mut carry = u128::from(addend);
772    for byte in result.iter_mut().rev() {
773        if carry == 0 {
774            break;
775        }
776        let value = u128::from(*byte) + (carry & 0xff);
777        *byte = (value & 0xff) as u8;
778        carry = (carry >> 8) + (value >> 8);
779    }
780    result
781}
782
783/// The `N` of a static array label such as `uint256[3]` or `Item[2][4]`; `None` for
784/// anything else.
785fn static_array_length(label: &str) -> Option<u64> {
786    let inner = label.strip_suffix(']')?;
787    let open = inner.rfind('[')?;
788    inner[open + 1..].parse::<u64>().ok()
789}
790
791/// Decodes the bytes of an in-place value by its Solidity type label.
792#[must_use]
793pub fn decode_value(bytes: &[u8], label: &str) -> String {
794    if label == "bool" {
795        return (bytes.iter().any(|byte| *byte != 0)).to_string();
796    }
797    if label == "address" || label == "address payable" || label.starts_with("contract ") {
798        return format!("0x{}", hex(&bytes[bytes.len().saturating_sub(20)..]));
799    }
800    if label.starts_with("bytes") && label.len() > 5 {
801        return format!("0x{}", hex(bytes));
802    }
803    if label.starts_with("int") {
804        return signed_to_decimal(bytes);
805    }
806    if label.starts_with("uint") || label.starts_with("enum ") {
807        return word_to_decimal(bytes);
808    }
809    format!("0x{}", hex(bytes))
810}
811
812/// A big-endian unsigned number as decimal text.
813#[must_use]
814pub fn word_to_decimal(bytes: &[u8]) -> String {
815    let mut digits = Vec::new();
816    let mut value = bytes.to_vec();
817    while value.iter().any(|byte| *byte != 0) {
818        let mut remainder = 0_u32;
819        for byte in value.iter_mut() {
820            let current = (remainder << 8) | u32::from(*byte);
821            *byte = (current / 10) as u8;
822            remainder = current % 10;
823        }
824        digits.push(b'0' + remainder as u8);
825    }
826    if digits.is_empty() {
827        return "0".to_owned();
828    }
829    digits.reverse();
830    String::from_utf8(digits).expect("ascii digits")
831}
832
833fn signed_to_decimal(bytes: &[u8]) -> String {
834    if bytes.first().is_some_and(|byte| byte & 0x80 != 0) {
835        // Negative: negate the two's complement to get the magnitude.
836        let mut magnitude = bytes.iter().map(|byte| !byte).collect::<Vec<_>>();
837        let mut carry = 1_u16;
838        for byte in magnitude.iter_mut().rev() {
839            let value = u16::from(*byte) + carry;
840            *byte = (value & 0xff) as u8;
841            carry = value >> 8;
842        }
843        return format!("-{}", word_to_decimal(&magnitude));
844    }
845    word_to_decimal(bytes)
846}
847
848/// A word as `0x`-prefixed hex.
849#[must_use]
850pub fn word_hex(word: &Word) -> String {
851    format!("0x{}", hex(word))
852}
853
854fn hex(bytes: &[u8]) -> String {
855    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
856}
857
858#[cfg(test)]
859mod tests {
860    use std::collections::BTreeMap;
861
862    use serde_json::json;
863
864    use super::{
865        add_word, decode_value, element_place, mapping_slot, parse_word, word_to_decimal,
866        StorageEncoding, StorageLayout, Word,
867    };
868    use crate::abi::keccak256;
869
870    fn layout() -> StorageLayout {
871        StorageLayout::parse(&json!({
872            "storage": [
873                {"astId": 4, "contract": "T.sol:T", "label": "counter", "offset": 0, "slot": "0", "type": "t_uint256"},
874                {"astId": 8, "contract": "T.sol:T", "label": "balances", "offset": 0, "slot": "1", "type": "t_mapping(t_address,t_uint256)"},
875                {"astId": 9, "contract": "T.sol:T", "label": "flag", "offset": 0, "slot": "2", "type": "t_bool"},
876                {"astId": 10, "contract": "T.sol:T", "label": "owner", "offset": 1, "slot": "2", "type": "t_address"},
877                {"astId": 11, "contract": "T.sol:T", "label": "delta", "offset": 21, "slot": "2", "type": "t_int8"},
878                {"astId": 12, "contract": "T.sol:T", "label": "name", "offset": 0, "slot": "3", "type": "t_string_storage"},
879                {"astId": 13, "contract": "T.sol:T", "label": "items", "offset": 0, "slot": "4", "type": "t_array(t_uint256)dyn_storage"},
880                {"astId": 14, "contract": "T.sol:T", "label": "config", "offset": 0, "slot": "5", "type": "t_struct(Config)15_storage"},
881                {"astId": 16, "contract": "T.sol:T", "label": "small", "offset": 0, "slot": "7", "type": "t_array(t_uint8)3_storage"},
882                {"astId": 17, "contract": "T.sol:T", "label": "names", "offset": 0, "slot": "8", "type": "t_mapping(t_string_memory_ptr,t_uint256)"}
883            ],
884            "types": {
885                "t_uint256": {"encoding": "inplace", "label": "uint256", "numberOfBytes": "32"},
886                "t_uint8": {"encoding": "inplace", "label": "uint8", "numberOfBytes": "1"},
887                "t_int8": {"encoding": "inplace", "label": "int8", "numberOfBytes": "1"},
888                "t_bool": {"encoding": "inplace", "label": "bool", "numberOfBytes": "1"},
889                "t_address": {"encoding": "inplace", "label": "address", "numberOfBytes": "20"},
890                "t_string_memory_ptr": {"encoding": "inplace", "label": "string", "numberOfBytes": "32"},
891                "t_string_storage": {"encoding": "bytes", "label": "string", "numberOfBytes": "32"},
892                "t_mapping(t_address,t_uint256)": {"encoding": "mapping", "key": "t_address", "label": "mapping(address => uint256)", "numberOfBytes": "32", "value": "t_uint256"},
893                "t_mapping(t_string_memory_ptr,t_uint256)": {"encoding": "mapping", "key": "t_string_memory_ptr", "label": "mapping(string => uint256)", "numberOfBytes": "32", "value": "t_uint256"},
894                "t_array(t_uint256)dyn_storage": {"encoding": "dynamic_array", "base": "t_uint256", "label": "uint256[]", "numberOfBytes": "32"},
895                "t_array(t_uint8)3_storage": {"encoding": "inplace", "base": "t_uint8", "label": "uint8[3]", "numberOfBytes": "32"},
896                "t_struct(Config)15_storage": {"encoding": "inplace", "label": "struct T.Config", "numberOfBytes": "64", "members": [
897                    {"astId": 1, "contract": "T.sol:T", "label": "owner", "offset": 0, "slot": "0", "type": "t_address"},
898                    {"astId": 2, "contract": "T.sol:T", "label": "limit", "offset": 0, "slot": "1", "type": "t_uint256"}
899                ]}
900            }
901        }))
902        .expect("layout")
903    }
904
905    fn word(number: u64) -> Word {
906        let mut word = [0_u8; 32];
907        word[24..].copy_from_slice(&number.to_be_bytes());
908        word
909    }
910
911    fn storage(entries: &[(Word, Word)]) -> BTreeMap<Word, Word> {
912        entries.iter().copied().collect()
913    }
914
915    #[test]
916    fn parses_the_layout_solc_writes() {
917        let layout = layout();
918        assert_eq!(layout.variables.len(), 10);
919        assert_eq!(layout.variable("balances").expect("balances").slot, word(1));
920        assert_eq!(layout.variable("owner").expect("owner").offset, 1);
921        let mapping = layout
922            .type_of("t_mapping(t_address,t_uint256)")
923            .expect("type");
924        assert_eq!(mapping.encoding, StorageEncoding::Mapping);
925        assert_eq!(mapping.key.as_deref(), Some("t_address"));
926        assert_eq!(
927            layout
928                .type_of("t_struct(Config)15_storage")
929                .expect("struct")
930                .members
931                .len(),
932            2
933        );
934        assert!(StorageLayout::parse(&json!({"types": {}})).is_err());
935    }
936
937    #[test]
938    fn resolves_variables_mappings_arrays_and_members() {
939        let layout = layout();
940        let counter = layout.resolve("counter").expect("counter");
941        assert_eq!((counter.slot, counter.offset), (word(0), 0));
942
943        let key = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
944        let entry = layout.resolve(&format!("balances[{key}]")).expect("entry");
945        let mut padded = [0_u8; 32];
946        padded[12..].copy_from_slice(&[
947            0xf3, 0x9f, 0xd6, 0xe5, 0x1a, 0xad, 0x88, 0xf6, 0xf4, 0xce, 0x6a, 0xb8, 0x82, 0x72,
948            0x79, 0xcf, 0xff, 0xb9, 0x22, 0x66,
949        ]);
950        assert_eq!(entry.slot, mapping_slot(&word(1), &padded));
951        assert_eq!(entry.type_id, "t_uint256");
952        assert_eq!(entry.path, format!("balances[{key}]"));
953        // Mixed-case and checksummed keys hash the same.
954        assert_eq!(
955            layout
956                .resolve("balances[0xF39FD6E51AAD88F6F4CE6AB8827279CFFFB92266]")
957                .expect("entry")
958                .slot,
959            entry.slot
960        );
961
962        // A string key hashes its bytes, not a padded word.
963        let by_name = layout.resolve("names[\"alice\"]").expect("string key");
964        assert_eq!(by_name.slot, mapping_slot(&word(8), b"alice"));
965
966        // Dynamic array elements start at keccak256(slot).
967        let second = layout.resolve("items[1]").expect("items[1]");
968        assert_eq!(second.slot, add_word(&keccak256(&word(4)), 1));
969        // Small static-array elements pack into one slot, least significant first.
970        let third = layout.resolve("small[2]").expect("small[2]");
971        assert_eq!((third.slot, third.offset), (word(7), 2));
972        assert!(layout.resolve("small[3]").is_err());
973
974        let limit = layout.resolve("config.limit").expect("member");
975        assert_eq!(
976            (limit.slot, limit.offset, limit.type_id.as_str()),
977            (word(6), 0, "t_uint256")
978        );
979
980        for bad in [
981            "missing",
982            "counter[1]",
983            "counter.x",
984            "config.nothing",
985            "balances",
986            "items[",
987            "counter+1",
988            "balances[]",
989        ] {
990            let outcome = layout.resolve(bad);
991            if bad == "balances" {
992                assert!(outcome.is_ok());
993            } else {
994                assert!(outcome.is_err(), "{bad}");
995            }
996        }
997    }
998
999    #[test]
1000    fn decodes_values_from_storage_words() {
1001        let layout = layout();
1002        let mut packed = [0_u8; 32];
1003        packed[31] = 1; // flag = true at offset 0
1004        packed[11..31].copy_from_slice(&[0xab; 20]); // owner at offset 1
1005        packed[10] = 0xfe; // delta = -2 at offset 21
1006        let long_text = "a string that is longer than thirty-one bytes, so it lives elsewhere";
1007        let mut long_head = [0_u8; 32];
1008        long_head[31] = (long_text.len() as u8) * 2 + 1;
1009        let data_slot = keccak256(&word(3));
1010        let mut words = storage(&[
1011            (word(0), word(42)),
1012            (word(2), packed),
1013            (word(3), long_head),
1014            (word(4), word(2)),
1015            (add_word(&keccak256(&word(4)), 1), word(7)),
1016            (word(5), {
1017                let mut owner = [0_u8; 32];
1018                owner[12..].copy_from_slice(&[0x11; 20]);
1019                owner
1020            }),
1021            (word(6), word(99)),
1022            (word(7), {
1023                let mut small = [0_u8; 32];
1024                small[31] = 1;
1025                small[30] = 2;
1026                small[29] = 3;
1027                small
1028            }),
1029        ]);
1030        for (index, chunk) in long_text.as_bytes().chunks(32).enumerate() {
1031            let mut piece = [0_u8; 32];
1032            piece[..chunk.len()].copy_from_slice(chunk);
1033            words.insert(add_word(&data_slot, index as u64), piece);
1034        }
1035        let read = |slot: &Word| words.get(slot).copied();
1036        let show = |path: &str| {
1037            layout
1038                .decode(&layout.resolve(path).expect(path), &read)
1039                .map(|value| value.display)
1040        };
1041
1042        assert_eq!(show("counter"), Ok("42".to_owned()));
1043        assert_eq!(show("flag"), Ok("true".to_owned()));
1044        assert_eq!(show("owner"), Ok(format!("0x{}", "ab".repeat(20))));
1045        assert_eq!(show("delta"), Ok("-2".to_owned()));
1046        assert_eq!(show("name"), Ok(format!("{long_text:?}")));
1047        assert_eq!(
1048            show("items"),
1049            Ok("<2 element(s); index it with [i]>".to_owned())
1050        );
1051        assert_eq!(show("items[1]"), Ok("7".to_owned()));
1052        assert_eq!(
1053            show("config"),
1054            Ok(format!("{{ owner: 0x{}, limit: 99 }}", "11".repeat(20)))
1055        );
1056        assert_eq!(show("small"), Ok("[1, 2, 3]".to_owned()));
1057        assert_eq!(
1058            show("balances"),
1059            Ok("<mapping; index it with [key]>".to_owned())
1060        );
1061        // A word the snapshot does not hold is reported by slot.
1062        assert_eq!(show("items[0]"), Err(keccak256(&word(4))));
1063
1064        // A short string sits in its own slot.
1065        let mut short = [0_u8; 32];
1066        short[..5].copy_from_slice(b"hello");
1067        short[31] = 10;
1068        let words = storage(&[(word(3), short)]);
1069        let read = |slot: &Word| words.get(slot).copied();
1070        assert_eq!(
1071            layout
1072                .decode(&layout.resolve("name").expect("name"), &read)
1073                .map(|value| value.display),
1074            Ok("\"hello\"".to_owned())
1075        );
1076    }
1077
1078    #[test]
1079    fn number_helpers_handle_256_bits() {
1080        assert_eq!(parse_word("255").expect("decimal"), word(255));
1081        assert_eq!(parse_word("0xff").expect("hex"), word(255));
1082        assert_eq!(
1083            word_to_decimal(&[0xff; 32]),
1084            "115792089237316195423570985008687907853269984665640564039457584007913129639935"
1085        );
1086        assert_eq!(word_to_decimal(&[0; 32]), "0");
1087        assert!(parse_word("1e3").is_err());
1088        assert!(parse_word("").is_err());
1089        let mut max = [0xff_u8; 32];
1090        assert_eq!(add_word(&max, 1), [0_u8; 32]);
1091        max[31] = 0xfe;
1092        assert_eq!(add_word(&max, 1), [0xff_u8; 32]);
1093        assert_eq!(element_place(&word(10), 5, 32), (word(15), 0));
1094        assert_eq!(element_place(&word(10), 5, 64), (word(20), 0));
1095        assert_eq!(element_place(&word(10), 33, 1), (word(11), 1));
1096        assert_eq!(decode_value(&[0x80], "int8"), "-128");
1097        assert_eq!(decode_value(&[0x7f], "int8"), "127");
1098        assert_eq!(decode_value(&[0x01, 0x02], "bytes2"), "0x0102");
1099        assert_eq!(decode_value(&[0x02], "enum T.Kind"), "2");
1100    }
1101}