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