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