Skip to main content

bal_layout/
lib.rs

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