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