Skip to main content

bal_layout/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! `bal-layout`: solc `storageLayout` → slot arithmetic and typed decoding.
4//!
5//! Knows nothing about where words come from. Give it a path like
6//! `balances[0xabc…]`, `totals.index`, `nested[0xabc…][7]`, `items[2]`,
7//! `items.length`, and it answers with a [`Location`] (slot, byte offset,
8//! size, type). Give it a 32-byte word and a location, and it decodes a
9//! [`Value`]. The reverse direction — "which field is slot X?" — works for
10//! everything except mapping entries (keccak is one-way; see
11//! [`Layout::describe_slot`]).
12
13use alloy_primitives::{keccak256, Address, B256, I256, U256};
14use serde::Deserialize;
15use std::collections::HashMap;
16use std::path::Path;
17
18/// Layout parsing and path resolution failures.
19#[derive(Debug, thiserror::Error)]
20pub enum LayoutError {
21    /// The layout JSON did not parse.
22    #[error("json: {0}")]
23    Json(String),
24    /// The artifact file could not be read.
25    #[error("io: {0}")]
26    Io(#[from] std::io::Error),
27    /// Neither a bare layout nor an artifact with a `storageLayout` key.
28    #[error("artifact has no storageLayout (compile with `extra_output = [\"storageLayout\"]` / outputSelection)")]
29    NoStorageLayout,
30    /// No such top-level variable or struct member.
31    #[error("unknown field `{0}`")]
32    UnknownField(String),
33    /// The layout references a type id it does not define.
34    #[error("unknown type id `{0}` in layout")]
35    UnknownType(String),
36    /// The path continues past something that cannot be indexed that way.
37    #[error("`{path}` is a {what}; expected {expected}")]
38    Shape {
39        /// Path resolved so far.
40        path: String,
41        /// What was found there.
42        what: &'static str,
43        /// What the next segment would have to be.
44        expected: &'static str,
45    },
46    /// A mapping key or array index did not parse.
47    #[error("bad key `{0}` for {1}")]
48    BadKey(String, String),
49    /// The path string is malformed.
50    #[error("bad path syntax near `{0}`")]
51    Syntax(String),
52    /// `string`/`bytes` mapping keys are hashed differently and not handled yet.
53    #[error("mapping keys of type {0} (dynamic) are not supported yet")]
54    DynamicKey(String),
55}
56
57/// Result of layout operations.
58pub type Result<T> = std::result::Result<T, LayoutError>;
59
60/// Deepest struct/array nesting followed. Real layouts are a few levels;
61/// a self-referential type in a crafted file would otherwise recurse forever.
62const MAX_NESTING: usize = 32;
63
64mod abi;
65mod ext;
66
67pub use abi::{encode_return, word_address, Getter, Getters, ResolvedCall};
68
69/// One variable (or struct member) as solc reports it.
70#[derive(Debug, Clone, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct StorageEntry {
73    /// Variable name.
74    pub label: String,
75    /// Slot number (relative to the struct start for members).
76    #[serde(deserialize_with = "de_u256_str")]
77    pub slot: U256,
78    /// Byte offset from the low-order end of the word.
79    pub offset: usize,
80    /// Type id, resolved through [`TypeInfo`].
81    #[serde(rename = "type")]
82    pub type_id: String,
83}
84
85/// One entry of the layout's `types` table.
86#[derive(Debug, Clone, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct TypeInfo {
89    /// How values of this type are laid out.
90    pub encoding: Encoding,
91    /// Solidity type name, e.g. `uint128`, `struct Foo.Bar`.
92    pub label: String,
93    /// Size in bytes (for arrays: of the whole array).
94    #[serde(deserialize_with = "de_usize_str")]
95    pub number_of_bytes: usize,
96    /// Mapping key type id.
97    pub key: Option<String>,
98    /// Mapping value type id.
99    pub value: Option<String>,
100    /// Array element type id.
101    pub base: Option<String>,
102    /// Struct members.
103    pub members: Option<Vec<StorageEntry>>,
104}
105
106/// solc storage encodings.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum Encoding {
110    /// Value stored in place (also structs and fixed arrays).
111    Inplace,
112    /// `keccak(key || slot)`.
113    Mapping,
114    /// Length in place, data at `keccak(slot)`.
115    DynamicArray,
116    /// `bytes`/`string`: short in place, long at `keccak(slot)`.
117    Bytes,
118}
119
120#[derive(Debug, Clone, Deserialize)]
121struct RawLayout {
122    storage: Vec<StorageEntry>,
123    types: HashMap<String, TypeInfo>,
124}
125
126fn de_u256_str<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<U256, D::Error> {
127    let s = String::deserialize(d)?;
128    s.parse::<U256>().map_err(serde::de::Error::custom)
129}
130
131fn de_usize_str<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<usize, D::Error> {
132    let s = String::deserialize(d)?;
133    s.parse::<usize>().map_err(serde::de::Error::custom)
134}
135
136/// Where a value lives.
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Location {
139    /// Storage slot.
140    pub slot: B256,
141    /// Byte offset from the low-order end of the word (solc convention).
142    pub offset: usize,
143    /// Size in bytes.
144    pub size: usize,
145    /// Type id for decoding.
146    pub type_id: String,
147}
148
149/// A decoded value. `Raw` means the layout knew the location but not how to
150/// read it (dynamic bytes/strings, unknown encodings): the word is shown as
151/// is rather than guessed.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum Value {
154    /// `uintN`, enums.
155    Uint(U256),
156    /// `intN`, sign-extended.
157    Int(I256),
158    /// `bool`.
159    Bool(bool),
160    /// `address`, contract types.
161    Address(Address),
162    /// `bytesN`.
163    FixedBytes(Vec<u8>),
164    /// Dynamic `bytes`, assembled from its data slots.
165    Bytes(Vec<u8>),
166    /// Dynamic `string`, assembled from its data slots.
167    Str(String),
168    /// Not decodable from a single word with this type; the full word.
169    Raw(B256),
170}
171
172impl std::fmt::Display for Value {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            Value::Uint(u) => write!(f, "{u}"),
176            Value::Int(i) => write!(f, "{i}"),
177            Value::Bool(b) => write!(f, "{b}"),
178            Value::Address(a) => write!(f, "{a}"),
179            Value::FixedBytes(b) | Value::Bytes(b) => {
180                write!(f, "0x{}", alloy_primitives::hex::encode(b))
181            }
182            Value::Str(s) => write!(f, "{s}"),
183            Value::Raw(w) => write!(f, "{w}"),
184        }
185    }
186}
187
188/// How a decoded value should be read by a caller that only sees text.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum ValueKind {
191    /// Unsigned integer or enum; decimal text.
192    Uint,
193    /// Signed integer; decimal text, possibly negative.
194    Int,
195    /// `true` / `false`.
196    Bool,
197    /// Checksummed `0x` address.
198    Address,
199    /// `bytesN`; `0x` hex.
200    Bytes,
201    /// Dynamic `bytes`: the slot word gives the length, the data may live
202    /// in further slots ([`Layout::bytes_data_slots`]); `0x` hex.
203    DynBytes,
204    /// Dynamic `string`, same layout as `DynBytes`; text.
205    String,
206    /// Not decodable from one word; the whole word as `0x` hex.
207    Raw,
208}
209
210/// What a storage path names.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum PathKind {
213    /// A leaf that can be read.
214    Value(ValueKind),
215    /// A struct: continue with `.member`.
216    Struct,
217    /// A mapping: continue with `[key]`.
218    Mapping,
219    /// A dynamic array: continue with `[index]` or `.length`.
220    Array,
221    /// A fixed-size array: continue with `[index]`.
222    FixedArray,
223}
224
225fn value_kind(label: &str) -> ValueKind {
226    if label == "bool" {
227        ValueKind::Bool
228    } else if label == "address" || label == "address payable" || label.starts_with("contract ") {
229        ValueKind::Address
230    } else if label.starts_with("uint") || label.starts_with("enum ") {
231        ValueKind::Uint
232    } else if label.starts_with("int") {
233        ValueKind::Int
234    } else if label.starts_with("bytes") {
235        ValueKind::Bytes
236    } else {
237        ValueKind::Raw
238    }
239}
240
241/// A parsed storage layout.
242pub struct Layout {
243    storage: Vec<StorageEntry>,
244    types: HashMap<String, TypeInfo>,
245}
246
247#[derive(Debug)]
248enum Seg {
249    Field(String),
250    Index(String),
251}
252
253fn parse_path(path: &str) -> Result<Vec<Seg>> {
254    let mut out = Vec::new();
255    let mut rest = path.trim();
256    if rest.is_empty() {
257        return Err(LayoutError::Syntax(path.into()));
258    }
259    let mut first = true;
260    while !rest.is_empty() {
261        if let Some(r) = rest.strip_prefix('[') {
262            let end = r
263                .find(']')
264                .ok_or_else(|| LayoutError::Syntax(rest.into()))?;
265            out.push(Seg::Index(r[..end].trim().to_string()));
266            rest = &r[end + 1..];
267        } else {
268            let r = if first {
269                rest
270            } else {
271                rest.strip_prefix('.')
272                    .ok_or_else(|| LayoutError::Syntax(rest.into()))?
273            };
274            let end = r.find(['.', '[']).unwrap_or(r.len());
275            if end == 0 {
276                return Err(LayoutError::Syntax(rest.into()));
277            }
278            out.push(Seg::Field(r[..end].to_string()));
279            rest = &r[end..];
280        }
281        first = false;
282    }
283    Ok(out)
284}
285
286/// Parse a mapping key or array index: decimal, `0x` hex, `true`/`false`,
287/// or a negative decimal (two's complement).
288fn parse_key(key: &str, key_type: &str) -> Result<B256> {
289    let k = key.trim();
290    let bad = || LayoutError::BadKey(key.into(), key_type.into());
291    let u = if let Some(h) = k.strip_prefix("0x") {
292        U256::from_str_radix(h, 16).map_err(|_| bad())?
293    } else if k == "true" {
294        U256::from(1)
295    } else if k == "false" {
296        U256::ZERO
297    } else if let Some(n) = k.strip_prefix('-') {
298        let n: U256 = n.parse().map_err(|_| bad())?;
299        U256::ZERO.wrapping_sub(n)
300    } else {
301        k.parse::<U256>().map_err(|_| bad())?
302    };
303    Ok(B256::from(u.to_be_bytes::<32>()))
304}
305
306fn slot_b(u: U256) -> B256 {
307    B256::from(u.to_be_bytes::<32>())
308}
309
310impl Layout {
311    /// Accepts either a bare `storageLayout` object or a whole forge/hardhat
312    /// artifact that contains one.
313    pub fn from_json(s: &str) -> Result<Self> {
314        let v: serde_json::Value =
315            serde_json::from_str(s).map_err(|e| LayoutError::Json(e.to_string()))?;
316        let raw = if v.get("storage").is_some() {
317            v
318        } else if let Some(l) = v.get("storageLayout") {
319            l.clone()
320        } else {
321            return Err(LayoutError::NoStorageLayout);
322        };
323        let raw: RawLayout =
324            serde_json::from_value(raw).map_err(|e| LayoutError::Json(e.to_string()))?;
325        Ok(Self {
326            storage: raw.storage,
327            types: raw.types,
328        })
329    }
330
331    /// [`Layout::from_json`] on the contents of `path`; a file with a
332    /// `namespaces` list is a manifest ([`Layout::from_manifest`]).
333    pub fn from_artifact(path: impl AsRef<Path>) -> Result<Self> {
334        let text = std::fs::read_to_string(path.as_ref())?;
335        if serde_json::from_str::<serde_json::Value>(&text)
336            .ok()
337            .is_some_and(|v| v.get("namespaces").is_some())
338        {
339            return Self::from_manifest(path);
340        }
341        Self::from_json(&text)
342    }
343
344    /// Top-level variables in declaration order.
345    pub fn fields(&self) -> impl Iterator<Item = &StorageEntry> {
346        self.storage.iter()
347    }
348
349    fn ty(&self, id: &str) -> Result<&TypeInfo> {
350        self.types
351            .get(id)
352            .ok_or_else(|| LayoutError::UnknownType(id.into()))
353    }
354
355    /// Resolve a dotted/indexed path to its storage location.
356    pub fn locate(&self, path: &str) -> Result<Location> {
357        let segs = parse_path(path)?;
358        let Some(Seg::Field(name)) = segs.first() else {
359            return Err(LayoutError::Syntax(path.into()));
360        };
361        let top = self
362            .storage
363            .iter()
364            .find(|e| &e.label == name)
365            .ok_or_else(|| LayoutError::UnknownField(name.clone()))?;
366        let mut slot = top.slot;
367        let mut offset = top.offset;
368        let mut type_id = top.type_id.clone();
369        let mut walked = name.clone();
370
371        for seg in &segs[1..] {
372            let t = self.ty(&type_id)?;
373            match (t.encoding, seg, t.base.as_deref(), t.members.as_deref()) {
374                (Encoding::Mapping, Seg::Index(k), _, _) => {
375                    let key_ty = t
376                        .key
377                        .as_deref()
378                        .ok_or_else(|| LayoutError::UnknownType(type_id.clone()))?;
379                    let kt = self.ty(key_ty)?;
380                    if kt.encoding == Encoding::Bytes {
381                        return Err(LayoutError::DynamicKey(kt.label.clone()));
382                    }
383                    let key = parse_key(k, &kt.label)?;
384                    let mut buf = [0u8; 64];
385                    buf[..32].copy_from_slice(key.as_slice());
386                    buf[32..].copy_from_slice(&slot.to_be_bytes::<32>());
387                    slot = U256::from_be_bytes(keccak256(buf).0);
388                    offset = 0;
389                    type_id = t
390                        .value
391                        .clone()
392                        .ok_or_else(|| LayoutError::UnknownType(type_id.clone()))?;
393                    walked = format!("{walked}[{k}]");
394                }
395                (Encoding::DynamicArray, Seg::Field(m), _, _) if m == "length" => {
396                    return Ok(Location {
397                        slot: slot_b(slot),
398                        offset: 0,
399                        size: 32,
400                        type_id: "t_uint256".into(),
401                    });
402                }
403                (Encoding::DynamicArray, Seg::Index(i), Some(base_ty), _) => {
404                    let idx = U256::from_be_bytes(parse_key(i, "index")?.0);
405                    let data = U256::from_be_bytes(keccak256(slot.to_be_bytes::<32>()).0);
406                    let (s, o) = self.element_at(base_ty, data, idx)?;
407                    slot = s;
408                    offset = o;
409                    type_id = base_ty.to_string();
410                    walked = format!("{walked}[{i}]");
411                }
412                (Encoding::Inplace, Seg::Index(i), Some(base_ty), _) => {
413                    // fixed-size array, in place
414                    let idx = U256::from_be_bytes(parse_key(i, "index")?.0);
415                    let (s, o) = self.element_at(base_ty, slot, idx)?;
416                    slot = s;
417                    offset = o;
418                    type_id = base_ty.to_string();
419                    walked = format!("{walked}[{i}]");
420                }
421                (Encoding::Inplace, Seg::Field(m), _, Some(members)) => {
422                    let member = members
423                        .iter()
424                        .find(|e| &e.label == m)
425                        .ok_or_else(|| LayoutError::UnknownField(format!("{walked}.{m}")))?;
426                    slot += member.slot;
427                    offset = member.offset;
428                    type_id = member.type_id.clone();
429                    walked = format!("{walked}.{m}");
430                }
431                (enc, _, _, _) => {
432                    let (what, expected) = match enc {
433                        Encoding::Mapping => ("mapping", "[key]"),
434                        Encoding::DynamicArray => ("dynamic array", "[index] or .length"),
435                        Encoding::Bytes => ("bytes/string", "no further path"),
436                        Encoding::Inplace => ("value", "no further path"),
437                    };
438                    return Err(LayoutError::Shape {
439                        path: walked,
440                        what,
441                        expected,
442                    });
443                }
444            }
445        }
446        let size = self.ty(&type_id)?.number_of_bytes;
447        Ok(Location {
448            slot: slot_b(slot),
449            offset,
450            size,
451            type_id,
452        })
453    }
454
455    /// Slot and offset of element `idx` of an array whose data starts at `data`.
456    fn element_at(&self, base_ty: &str, data: U256, idx: U256) -> Result<(U256, usize)> {
457        let size = self.ty(base_ty)?.number_of_bytes;
458        if size == 0 {
459            return Err(LayoutError::UnknownType(base_ty.into()));
460        }
461        // Slot arithmetic is modulo 2^256, like the EVM's; an absurd index
462        // wraps instead of panicking.
463        if size >= 32 {
464            let per_elem = U256::from(size.div_ceil(32));
465            Ok((data.wrapping_add(idx.wrapping_mul(per_elem)), 0))
466        } else {
467            let per_slot = U256::from(32 / size);
468            let slot = data.wrapping_add(idx / per_slot);
469            let offset = (idx % per_slot).to::<usize>() * size;
470            Ok((slot, offset))
471        }
472    }
473
474    /// Decode the value at `loc` out of a full 32-byte storage word.
475    pub fn decode(&self, loc: &Location, word: B256) -> Value {
476        let size = loc.size.clamp(1, 32);
477        let end = 32usize.saturating_sub(loc.offset).max(1);
478        let start = end.saturating_sub(size);
479        let bytes = &word.0[start..end];
480        let Some(t) = self.types.get(&loc.type_id) else {
481            return Value::Raw(word);
482        };
483        let label = t.label.as_str();
484        if t.encoding != Encoding::Inplace || t.members.is_some() || t.base.is_some() {
485            return Value::Raw(word);
486        }
487        if label == "bool" {
488            return Value::Bool(bytes.iter().any(|b| *b != 0));
489        }
490        if label == "address" || label == "address payable" || label.starts_with("contract ") {
491            let n = bytes.len();
492            return Value::Address(Address::from_slice(&bytes[n.saturating_sub(20)..]));
493        }
494        if label.starts_with("uint") || label.starts_with("enum ") {
495            return Value::Uint(U256::from_be_slice(bytes));
496        }
497        if label.starts_with("int") {
498            let fill = if bytes[0] & 0x80 != 0 { 0xFF } else { 0x00 };
499            let mut w = [fill; 32];
500            w[32 - bytes.len()..].copy_from_slice(bytes);
501            return Value::Int(I256::from_be_bytes(w));
502        }
503        if label.starts_with("bytes") && size <= 32 {
504            // bytesN are left-aligned inside their `size` bytes
505            return Value::FixedBytes(bytes.to_vec());
506        }
507        Value::Raw(word)
508    }
509
510    /// What kind of thing a path names — a leaf value, or a container that
511    /// takes another path segment. Drives the Node `view` proxy.
512    pub fn kind_of(&self, path: &str) -> Result<PathKind> {
513        let loc = self.locate(path)?;
514        if path.trim_end().ends_with(".length") {
515            return Ok(PathKind::Value(ValueKind::Uint));
516        }
517        let t = self.ty(&loc.type_id)?;
518        Ok(match (t.encoding, t.members.is_some(), t.base.is_some()) {
519            (Encoding::Mapping, _, _) => PathKind::Mapping,
520            (Encoding::DynamicArray, _, _) => PathKind::Array,
521            (Encoding::Inplace, true, _) => PathKind::Struct,
522            (Encoding::Inplace, false, true) => PathKind::FixedArray,
523            (Encoding::Bytes, _, _) => PathKind::Value(if t.label.starts_with("string") {
524                ValueKind::String
525            } else {
526                ValueKind::DynBytes
527            }),
528            (Encoding::Inplace, false, false) => PathKind::Value(value_kind(&t.label)),
529        })
530    }
531
532    /// Decode with an explicit kind, so callers that marshal across a
533    /// language boundary know what the text is.
534    pub fn decode_typed(&self, loc: &Location, word: B256) -> (ValueKind, Value) {
535        let v = self.decode(loc, word);
536        let k = match &v {
537            Value::Uint(_) => ValueKind::Uint,
538            Value::Int(_) => ValueKind::Int,
539            Value::Bool(_) => ValueKind::Bool,
540            Value::Address(_) => ValueKind::Address,
541            Value::FixedBytes(_) => ValueKind::Bytes,
542            Value::Bytes(_) => ValueKind::DynBytes,
543            Value::Str(_) => ValueKind::String,
544            Value::Raw(_) => ValueKind::Raw,
545        };
546        (k, v)
547    }
548
549    /// TypeScript declaration of the contract's storage as the Node `view`
550    /// exposes it: `bigint` for integers, `boolean`, `string` for addresses
551    /// and bytes, nested objects for structs, index signatures for mappings
552    /// and arrays. `name` is the interface name.
553    pub fn typescript(&self, name: &str) -> String {
554        let mut out = String::new();
555        out.push_str("// Generated by balq from a solc storageLayout. Do not edit.\n");
556        out.push_str(&format!("export interface {name} {{\n"));
557        for e in &self.storage {
558            out.push_str(&format!(
559                "  readonly {}: {};\n",
560                e.label,
561                self.ts_type(&e.type_id, 1)
562            ));
563        }
564        out.push_str("}\n");
565        out
566    }
567
568    fn ts_type(&self, type_id: &str, depth: usize) -> String {
569        // A crafted layout can reference itself; solc never emits that, but a
570        // file is not solc. Stop instead of overflowing the stack.
571        if depth > MAX_NESTING {
572            return "unknown".into();
573        }
574        let Ok(t) = self.ty(type_id) else {
575            return "string".into();
576        };
577        let pad = "  ".repeat(depth + 1);
578        let close = "  ".repeat(depth);
579        match (t.encoding, t.members.as_deref(), t.base.as_deref()) {
580            (Encoding::Mapping, _, _) => {
581                let v = t
582                    .value
583                    .as_deref()
584                    .map(|v| self.ts_type(v, depth + 1))
585                    .unwrap_or_else(|| "string".into());
586                format!("{{ readonly [key: string]: {v} }}")
587            }
588            (Encoding::DynamicArray, _, base) => {
589                let v = base
590                    .map(|b| self.ts_type(b, depth + 1))
591                    .unwrap_or_else(|| "string".into());
592                format!("{{ readonly [index: number]: {v}; readonly length: bigint }}")
593            }
594            (Encoding::Inplace, Some(members), _) => {
595                let mut s = String::from("{\n");
596                for m in members {
597                    s.push_str(&format!(
598                        "{pad}readonly {}: {};\n",
599                        m.label,
600                        self.ts_type(&m.type_id, depth + 1)
601                    ));
602                }
603                s.push_str(&format!("{close}}}"));
604                s
605            }
606            (Encoding::Inplace, None, Some(base)) => {
607                format!(
608                    "{{ readonly [index: number]: {} }}",
609                    self.ts_type(base, depth + 1)
610                )
611            }
612            (Encoding::Bytes, _, _) => "string".into(),
613            (Encoding::Inplace, None, None) => match value_kind(&t.label) {
614                ValueKind::Uint | ValueKind::Int => "bigint".into(),
615                ValueKind::Bool => "boolean".into(),
616                ValueKind::Address
617                | ValueKind::Bytes
618                | ValueKind::DynBytes
619                | ValueKind::String
620                | ValueKind::Raw => "string".into(),
621            },
622        }
623    }
624
625    /// Human name(s) for a raw slot: top-level values, struct members, and
626    /// dynamic-array elements within `array_probe` slots of the data start.
627    /// Mapping entries cannot be named without a candidate key.
628    pub fn describe_slot(&self, slot: B256, array_probe: u64) -> Vec<(String, Location)> {
629        let target = U256::from_be_bytes(slot.0);
630        let mut out = Vec::new();
631        for e in &self.storage {
632            self.describe_in(
633                &e.label,
634                e.slot,
635                e.offset,
636                &e.type_id,
637                target,
638                array_probe,
639                &mut out,
640            );
641        }
642        out
643    }
644
645    #[allow(clippy::too_many_arguments)]
646    fn describe_in(
647        &self,
648        name: &str,
649        base: U256,
650        offset: usize,
651        type_id: &str,
652        target: U256,
653        probe: u64,
654        out: &mut Vec<(String, Location)>,
655    ) {
656        // Recursion depth is bounded by the path length being built.
657        if name.matches(['.', '[']).count() > MAX_NESTING {
658            return;
659        }
660        let Ok(t) = self.ty(type_id) else { return };
661        match (t.encoding, t.members.as_deref(), t.base.as_deref()) {
662            (Encoding::Inplace, Some(members), _) => {
663                for m in members {
664                    self.describe_in(
665                        &format!("{name}.{}", m.label),
666                        base + m.slot,
667                        m.offset,
668                        &m.type_id,
669                        target,
670                        probe,
671                        out,
672                    );
673                }
674            }
675            (Encoding::Inplace, None, Some(base_ty)) => {
676                let words = U256::from(t.number_of_bytes.div_ceil(32));
677                if target < base || target >= base + words {
678                    return;
679                }
680                self.describe_elements(name, base, base_ty, target, probe, out);
681            }
682            (Encoding::Inplace, None, None) | (Encoding::Bytes, _, _) => {
683                let words = U256::from(t.number_of_bytes.div_ceil(32).max(1));
684                if target >= base && target < base + words {
685                    out.push((
686                        name.to_string(),
687                        Location {
688                            slot: slot_b(target),
689                            offset,
690                            size: t.number_of_bytes,
691                            type_id: type_id.to_string(),
692                        },
693                    ));
694                }
695            }
696            (Encoding::DynamicArray, _, base_ty) => {
697                if target == base {
698                    out.push((
699                        format!("{name}.length"),
700                        Location {
701                            slot: slot_b(base),
702                            offset: 0,
703                            size: 32,
704                            type_id: "t_uint256".into(),
705                        },
706                    ));
707                    return;
708                }
709                let data = U256::from_be_bytes(keccak256(base.to_be_bytes::<32>()).0);
710                if target < data || target - data >= U256::from(probe) {
711                    return;
712                }
713                let Some(base_ty) = base_ty else { return };
714                self.describe_elements(name, data, base_ty, target, probe, out);
715            }
716            (Encoding::Mapping, _, _) => {}
717        }
718    }
719
720    /// Name the element(s) of an array whose data starts at `data` that live
721    /// in slot `target`. Multi-word elements (structs, nested arrays) recurse
722    /// so their members get named too.
723    fn describe_elements(
724        &self,
725        name: &str,
726        data: U256,
727        base_ty: &str,
728        target: U256,
729        probe: u64,
730        out: &mut Vec<(String, Location)>,
731    ) {
732        let Ok(bt) = self.ty(base_ty) else { return };
733        let size = bt.number_of_bytes;
734        if size == 0 {
735            return; // a crafted layout; never divide by it
736        }
737        if size >= 32 {
738            let per_elem = size.div_ceil(32) as u64;
739            let i = (target - data).to::<u64>() / per_elem;
740            self.describe_in(
741                &format!("{name}[{i}]"),
742                data + U256::from(i * per_elem),
743                0,
744                base_ty,
745                target,
746                probe,
747                out,
748            );
749        } else {
750            let per = (32 / size) as u64;
751            let first = (target - data).to::<u64>() * per;
752            for i in first..first + per {
753                out.push((
754                    format!("{name}[{i}]"),
755                    Location {
756                        slot: slot_b(target),
757                        offset: ((i % per) as usize) * size,
758                        size,
759                        type_id: base_ty.to_string(),
760                    },
761                ));
762            }
763        }
764    }
765}
766
767#[cfg(test)]
768mod tests {
769    use super::*;
770
771    const PLAYGROUND: &str = include_str!("../tests/fixtures/Playground.layout.json");
772
773    fn layout() -> Layout {
774        Layout::from_json(PLAYGROUND).unwrap()
775    }
776
777    fn s(n: u64) -> B256 {
778        slot_b(U256::from(n))
779    }
780
781    #[test]
782    fn flat_and_packed() {
783        let l = layout();
784        assert_eq!(
785            l.locate("counter").unwrap(),
786            Location {
787                slot: s(0),
788                offset: 0,
789                size: 32,
790                type_id: "t_uint256".into()
791            }
792        );
793        let b = l.locate("b").unwrap();
794        assert_eq!((b.slot, b.offset, b.size), (s(1), 16, 8));
795        let c = l.locate("c").unwrap();
796        assert_eq!((c.slot, c.offset, c.size), (s(1), 24, 1));
797        // word = a=5 | b=7<<128 | c=1<<192
798        let word = slot_b(U256::from(5) | (U256::from(7) << 128) | (U256::from(1) << 192));
799        assert_eq!(
800            l.decode(&l.locate("a").unwrap(), word),
801            Value::Uint(U256::from(5))
802        );
803        assert_eq!(l.decode(&b, word), Value::Uint(U256::from(7)));
804        assert_eq!(l.decode(&c, word), Value::Bool(true));
805    }
806
807    #[test]
808    fn struct_members() {
809        let l = layout();
810        let idx = l.locate("totals.index").unwrap();
811        assert_eq!((idx.slot, idx.offset, idx.size), (s(2), 8, 24));
812        assert!(matches!(
813            l.locate("totals.nope"),
814            Err(LayoutError::UnknownField(_))
815        ));
816    }
817
818    #[test]
819    fn mappings() {
820        let l = layout();
821        let addr = "0x000000000000000000000000000000000000dEaD";
822        let loc = l.locate(&format!("balances[{addr}]")).unwrap();
823        let mut buf = [0u8; 64];
824        buf[12..32].copy_from_slice(&alloy_primitives::hex::decode(&addr[2..]).unwrap());
825        buf[63] = 3;
826        assert_eq!(loc.slot, keccak256(buf));
827        let inner = l.locate(&format!("nested[{addr}][7]")).unwrap();
828        buf[63] = 4;
829        let first = keccak256(buf);
830        let mut buf2 = [0u8; 64];
831        buf2[31] = 7;
832        buf2[32..].copy_from_slice(first.as_slice());
833        assert_eq!(inner.slot, keccak256(buf2));
834        assert!(matches!(
835            l.locate("balances.x"),
836            Err(LayoutError::Shape { .. })
837        ));
838    }
839
840    #[test]
841    fn dynamic_array_and_describe() {
842        let l = layout();
843        assert_eq!(l.locate("items.length").unwrap().slot, s(5));
844        let data = U256::from_be_bytes(keccak256(U256::from(5).to_be_bytes::<32>()).0);
845        assert_eq!(
846            l.locate("items[2]").unwrap().slot,
847            slot_b(data + U256::from(2))
848        );
849
850        let names = |slot: B256| -> Vec<String> {
851            l.describe_slot(slot, 64)
852                .into_iter()
853                .map(|(n, _)| n)
854                .collect()
855        };
856        assert_eq!(names(s(1)), vec!["a", "b", "c"]);
857        assert_eq!(names(s(2)), vec!["totals.lastTime", "totals.index"]);
858        assert_eq!(names(slot_b(data + U256::from(3))), vec!["items[3]"]);
859        assert!(names(s(99)).is_empty());
860    }
861
862    #[test]
863    fn kinds_and_typescript() {
864        let l = layout();
865        assert_eq!(
866            l.kind_of("counter").unwrap(),
867            PathKind::Value(ValueKind::Uint)
868        );
869        assert_eq!(l.kind_of("c").unwrap(), PathKind::Value(ValueKind::Bool));
870        assert_eq!(
871            l.kind_of("lastPoker").unwrap(),
872            PathKind::Value(ValueKind::Address)
873        );
874        assert_eq!(l.kind_of("totals").unwrap(), PathKind::Struct);
875        assert_eq!(l.kind_of("balances").unwrap(), PathKind::Mapping);
876        assert_eq!(l.kind_of("nested[0x1]").unwrap(), PathKind::Mapping);
877        assert_eq!(l.kind_of("items").unwrap(), PathKind::Array);
878        assert_eq!(
879            l.kind_of("items.length").unwrap(),
880            PathKind::Value(ValueKind::Uint)
881        );
882        assert_eq!(
883            l.kind_of("items[2]").unwrap(),
884            PathKind::Value(ValueKind::Uint)
885        );
886
887        let ts = l.typescript("PlaygroundView");
888        assert!(ts.contains("export interface PlaygroundView {"));
889        assert!(ts.contains("readonly counter: bigint;"));
890        assert!(ts.contains("readonly c: boolean;"));
891        assert!(ts.contains("readonly lastPoker: string;"));
892        assert!(ts.contains("readonly balances: { readonly [key: string]: bigint };"));
893        assert!(ts.contains(
894            "readonly nested: { readonly [key: string]: { readonly [key: string]: bigint } };"
895        ));
896        assert!(ts.contains(
897            "readonly items: { readonly [index: number]: bigint; readonly length: bigint };"
898        ));
899        assert!(ts.contains(
900            "readonly totals: {\n    readonly lastTime: bigint;\n    readonly index: bigint;\n  };"
901        ));
902    }
903
904    /// A layout is a file, not solc: a type that contains itself must not
905    /// recurse forever in `typescript()` or `describe_slot()`.
906    #[test]
907    fn self_referential_layout_terminates() {
908        let json = r#"{
909          "storage": [{"label":"a","slot":"0","offset":0,"type":"t_struct(A)"}],
910          "types": {
911            "t_struct(A)": {"encoding":"inplace","label":"struct A","numberOfBytes":"64",
912              "members":[{"label":"inner","slot":"0","offset":0,"type":"t_struct(A)"},
913                         {"label":"n","slot":"1","offset":0,"type":"t_uint256"}]},
914            "t_uint256": {"encoding":"inplace","label":"uint256","numberOfBytes":"32"}
915          }}"#;
916        let l = Layout::from_json(json).unwrap();
917        let ts = l.typescript("Evil");
918        assert!(ts.contains("unknown"), "recursion must be cut, got:\n{ts}");
919
920        // The same through mapping / array / fixed-array self-references,
921        // plus an element type of zero bytes.
922        let json2 = r#"{
923          "storage": [
924            {"label":"m","slot":"0","offset":0,"type":"t_m"},
925            {"label":"d","slot":"1","offset":0,"type":"t_d"},
926            {"label":"f","slot":"2","offset":0,"type":"t_f"},
927            {"label":"z","slot":"3","offset":0,"type":"t_z"}],
928          "types": {
929            "t_m": {"encoding":"mapping","label":"mapping(uint256 => m)","numberOfBytes":"32","key":"t_uint256","value":"t_m"},
930            "t_d": {"encoding":"dynamic_array","label":"d[]","numberOfBytes":"32","base":"t_d"},
931            "t_f": {"encoding":"inplace","label":"f[2]","numberOfBytes":"64","base":"t_f"},
932            "t_z": {"encoding":"dynamic_array","label":"zero[]","numberOfBytes":"32","base":"t_zero"},
933            "t_zero": {"encoding":"inplace","label":"uint0","numberOfBytes":"0"},
934            "t_uint256": {"encoding":"inplace","label":"uint256","numberOfBytes":"32"}
935          }}"#;
936        let l2 = Layout::from_json(json2).unwrap();
937        let _ = l2.typescript("Evil2");
938        let data = U256::from_be_bytes(keccak256(U256::from(3).to_be_bytes::<32>()).0);
939        let _ = l2.describe_slot(slot_b(data), 16); // zero-byte element: no division by zero
940        let _ = l2.describe_slot(s(2), 16);
941        let names = l.describe_slot(s(1), 16);
942        assert!(names.iter().any(|(n, _)| n.ends_with(".n")));
943        // path resolution itself is iterative and bounded by the path length
944        assert!(l.locate("a.inner.inner.n").is_ok());
945    }
946
947    #[test]
948    fn decode_address_and_never_panics() {
949        let l = layout();
950        let loc = l.locate("lastPoker").unwrap();
951        let w = slot_b(U256::from_be_slice(&[0xAB; 20]));
952        assert_eq!(
953            l.decode(&loc, w),
954            Value::Address(Address::repeat_byte(0xAB))
955        );
956        let bogus = Location {
957            slot: s(0),
958            offset: 40,
959            size: 64,
960            type_id: "t_uint256".into(),
961        };
962        let _ = l.decode(&bogus, B256::ZERO);
963    }
964}