Skip to main content

bal_layout/
abi.rs

1//! `eth_call` on a compiler-generated getter is a storage read. For every
2//! `public` state variable Solidity emits a getter with a fixed shape:
3//!
4//! - a value: `x()` returns it;
5//! - a mapping: `x(key)`, nested mappings take one key per level;
6//! - an array: `x(index)`; a mapping to an array: `x(key, index)`;
7//! - a struct: the members in order, *except* mappings and dynamic arrays.
8//!
9//! So a call whose selector names a function `x(...)` with exactly that
10//! shape, where `x` is a top-level variable of the layout, reads a known
11//! path — and the answer can come from the archive, ABI-encoded, without an
12//! EVM. Anything else (a view with logic, a mismatched shape) is *not*
13//! resolved; the caller falls back to a node. A guess would break the
14//! promise that balq never answers with something it does not know.
15
16use crate::{Encoding, Layout, LayoutError, Location, Result, Value};
17use alloy_primitives::{keccak256, Address, B256, I256, U256};
18
19/// One `view`/`pure` function of an ABI, as far as a getter needs.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Getter {
22    /// Function name.
23    pub name: String,
24    /// `keccak256("name(t1,t2)")[..4]`.
25    pub selector: [u8; 4],
26    /// Canonical input types (`address`, `uint256`, …).
27    pub inputs: Vec<String>,
28    /// Canonical output types.
29    pub outputs: Vec<String>,
30}
31
32/// The `view` functions of a contract ABI, by selector.
33#[derive(Debug, Clone, Default)]
34pub struct Getters(Vec<Getter>);
35
36impl Getters {
37    /// From an artifact's `abi` array (forge / hardhat / solc output).
38    /// Functions that are not `view`/`pure`, or whose inputs are not
39    /// static single-word types, are skipped: they can never be getters.
40    pub fn from_abi(abi: &serde_json::Value) -> Self {
41        let mut out = Vec::new();
42        let Some(items) = abi.as_array() else {
43            return Self(out);
44        };
45        for f in items {
46            if f["type"].as_str() != Some("function") {
47                continue;
48            }
49            if !matches!(f["stateMutability"].as_str(), Some("view") | Some("pure")) {
50                continue;
51            }
52            let Some(name) = f["name"].as_str() else {
53                continue;
54            };
55            let types = |k: &str| -> Option<Vec<String>> {
56                f[k].as_array()?
57                    .iter()
58                    .map(|p| p["type"].as_str().map(String::from))
59                    .collect()
60            };
61            let (Some(inputs), Some(outputs)) = (types("inputs"), types("outputs")) else {
62                continue;
63            };
64            if !inputs.iter().all(|t| is_static_word(t)) {
65                continue;
66            }
67            let sig = format!("{name}({})", inputs.join(","));
68            let h = keccak256(sig.as_bytes());
69            out.push(Getter {
70                name: name.to_string(),
71                selector: [h[0], h[1], h[2], h[3]],
72                inputs,
73                outputs,
74            });
75        }
76        Self(out)
77    }
78
79    /// From a whole artifact (`{ "abi": [...] , ... }`); empty if it has none.
80    pub fn from_artifact_json(v: &serde_json::Value) -> Self {
81        v.get("abi").map(Self::from_abi).unwrap_or_default()
82    }
83
84    /// The function with this selector, if any.
85    pub fn find(&self, selector: &[u8; 4]) -> Option<&Getter> {
86        self.0.iter().find(|g| &g.selector == selector)
87    }
88
89    /// No `view` functions known.
90    pub fn is_empty(&self) -> bool {
91        self.0.is_empty()
92    }
93
94    /// Number of `view` functions known.
95    pub fn len(&self) -> usize {
96        self.0.len()
97    }
98}
99
100/// A call resolved to storage: the path it reads and how to encode the answer.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ResolvedCall {
103    /// The getter that matched.
104    pub name: String,
105    /// Storage path of what it reads (`balances[0x…]`, `totals`).
106    pub path: String,
107    /// One location per output, in output order (a struct getter has several).
108    pub reads: Vec<Location>,
109    /// Canonical output types, parallel to `reads`.
110    pub outputs: Vec<String>,
111}
112
113fn is_static_word(t: &str) -> bool {
114    t == "address"
115        || t == "bool"
116        || (t.starts_with("uint") || t.starts_with("int")) && !t.contains('[')
117        || (t.starts_with("bytes") && t.len() > 5 && t[5..].parse::<u8>().is_ok())
118}
119
120/// The ABI type a storage type label corresponds to.
121fn abi_type_of(label: &str) -> String {
122    if label == "address payable" || label.starts_with("contract ") {
123        return "address".into();
124    }
125    if label.starts_with("enum ") {
126        return "uint8".into();
127    }
128    if label == "string storage ref" {
129        return "string".into();
130    }
131    if label == "bytes storage ref" {
132        return "bytes".into();
133    }
134    label.to_string()
135}
136
137/// A 32-byte calldata word as the text `Layout::locate` takes for a key or
138/// index: the full word in hex, so `bytesN` keys keep their left alignment
139/// and integers keep their value.
140fn word_text(w: &[u8]) -> String {
141    format!("0x{}", alloy_primitives::hex::encode(w))
142}
143
144impl Layout {
145    /// Resolve `calldata` (selector + ABI-encoded arguments) against the
146    /// contract's getters and this layout. `Ok(None)` means "not a getter of
147    /// a variable in this layout" — fall back to a node. `Err` only for
148    /// malformed calldata.
149    pub fn resolve_call(&self, getters: &Getters, calldata: &[u8]) -> Result<Option<ResolvedCall>> {
150        if calldata.len() < 4 {
151            return Err(LayoutError::Syntax(
152                "calldata shorter than a selector".into(),
153            ));
154        }
155        let selector: [u8; 4] = [calldata[0], calldata[1], calldata[2], calldata[3]];
156        let Some(g) = getters.find(&selector) else {
157            return Ok(None);
158        };
159        let args = &calldata[4..];
160        if args.len() != 32 * g.inputs.len() {
161            return Err(LayoutError::Syntax(format!(
162                "{}: expected {} argument word(s), got {} bytes",
163                g.name,
164                g.inputs.len(),
165                args.len()
166            )));
167        }
168        // The getter's name must be a top-level variable.
169        let Some(top) = self.storage.iter().find(|e| e.label == g.name) else {
170            return Ok(None);
171        };
172        // Walk the type chain: one input per mapping key / array index.
173        let mut path = g.name.clone();
174        let mut type_id = top.type_id.clone();
175        for (i, in_ty) in g.inputs.iter().enumerate() {
176            let Ok(t) = self.ty(&type_id) else {
177                return Ok(None);
178            };
179            let word = &args[32 * i..32 * i + 32];
180            match t.encoding {
181                Encoding::Mapping => {
182                    let (Some(kt), Some(vt)) = (t.key.as_deref(), t.value.as_deref()) else {
183                        return Ok(None);
184                    };
185                    let key_abi = self
186                        .ty(kt)
187                        .map(|k| abi_type_of(&k.label))
188                        .unwrap_or_default();
189                    if &key_abi != in_ty {
190                        return Ok(None);
191                    }
192                    path = format!("{path}[{}]", word_text(word));
193                    type_id = vt.to_string();
194                }
195                Encoding::DynamicArray | Encoding::Inplace if t.base.is_some() => {
196                    if in_ty != "uint256" {
197                        return Ok(None);
198                    }
199                    let idx = U256::from_be_slice(word);
200                    path = format!("{path}[{idx}]");
201                    type_id = t.base.clone().unwrap_or_default();
202                }
203                _ => return Ok(None),
204            }
205        }
206        // What is left must be what the outputs describe.
207        let Ok(t) = self.ty(&type_id) else {
208            return Ok(None);
209        };
210        let (reads, outputs): (Vec<Location>, Vec<String>) =
211            match (t.encoding, t.members.as_deref(), t.base.is_some()) {
212                (Encoding::Inplace, Some(members), _) => {
213                    // Struct: members in order, minus mappings and dynamic arrays.
214                    let mut reads = Vec::new();
215                    let mut outs = Vec::new();
216                    for m in members {
217                        let Ok(mt) = self.ty(&m.type_id) else {
218                            return Ok(None);
219                        };
220                        if mt.encoding == Encoding::Mapping || mt.encoding == Encoding::DynamicArray
221                        {
222                            continue;
223                        }
224                        if mt.members.is_some() || mt.base.is_some() {
225                            return Ok(None); // nested struct / fixed array: not a plain tuple
226                        }
227                        let Ok(loc) = self.locate(&format!("{path}.{}", m.label)) else {
228                            return Ok(None);
229                        };
230                        reads.push(loc);
231                        outs.push(abi_type_of(&mt.label));
232                    }
233                    (reads, outs)
234                }
235                (Encoding::Inplace, None, false) | (Encoding::Bytes, _, _) => {
236                    let Ok(loc) = self.locate(&path) else {
237                        return Ok(None);
238                    };
239                    (vec![loc], vec![abi_type_of(&t.label)])
240                }
241                _ => return Ok(None), // a container needs more inputs than given
242            };
243        if outputs != g.outputs {
244            return Ok(None);
245        }
246        Ok(Some(ResolvedCall {
247            name: g.name.clone(),
248            path,
249            reads,
250            outputs,
251        }))
252    }
253}
254
255/// ABI-encode decoded values as a function's return data. Static types take
256/// one word each; `string`/`bytes` are dynamic (offset, length, data).
257pub fn encode_return(values: &[Value], types: &[String]) -> Result<Vec<u8>> {
258    if values.len() != types.len() {
259        return Err(LayoutError::Syntax("values/types length mismatch".into()));
260    }
261    let n = values.len();
262    let mut head: Vec<[u8; 32]> = Vec::with_capacity(n);
263    let mut tail: Vec<u8> = Vec::new();
264    for (v, t) in values.iter().zip(types) {
265        let dynamic = t == "string" || t == "bytes";
266        if dynamic {
267            let bytes: Vec<u8> = match v {
268                Value::Str(s) => s.as_bytes().to_vec(),
269                Value::Bytes(b) | Value::FixedBytes(b) => b.clone(),
270                _ => return Err(LayoutError::Syntax(format!("{t}: not a byte value"))),
271            };
272            let offset = U256::from(32 * n + tail.len());
273            head.push(offset.to_be_bytes::<32>());
274            tail.extend_from_slice(&U256::from(bytes.len()).to_be_bytes::<32>());
275            tail.extend_from_slice(&bytes);
276            let pad = (32 - bytes.len() % 32) % 32;
277            tail.extend(std::iter::repeat_n(0u8, pad));
278        } else {
279            head.push(static_word(v, t)?);
280        }
281    }
282    let mut out = Vec::with_capacity(32 * n + tail.len());
283    for w in head {
284        out.extend_from_slice(&w);
285    }
286    out.extend_from_slice(&tail);
287    Ok(out)
288}
289
290fn static_word(v: &Value, t: &str) -> Result<[u8; 32]> {
291    Ok(match v {
292        Value::Uint(u) => u.to_be_bytes::<32>(),
293        Value::Int(i) => i.to_be_bytes::<32>(),
294        Value::Bool(b) => U256::from(*b as u8).to_be_bytes::<32>(),
295        Value::Address(a) => B256::left_padding_from(a.as_slice()).0,
296        Value::FixedBytes(b) => {
297            let mut w = [0u8; 32];
298            let n = b.len().min(32);
299            w[..n].copy_from_slice(&b[..n]);
300            w
301        }
302        Value::Raw(w) => {
303            if t.starts_with("int") {
304                I256::from_raw(U256::from_be_bytes(w.0)).to_be_bytes::<32>()
305            } else {
306                w.0
307            }
308        }
309        Value::Str(_) | Value::Bytes(_) => {
310            return Err(LayoutError::Syntax(format!(
311                "{t}: dynamic value in a static slot"
312            )))
313        }
314    })
315}
316
317/// `Address` from a 32-byte calldata word (the low 20 bytes).
318pub fn word_address(w: &[u8; 32]) -> Address {
319    Address::from_slice(&w[12..])
320}
321
322#[cfg(test)]
323mod tests {
324    #![allow(clippy::unwrap_used)]
325    use super::*;
326
327    const ARTIFACT: &str = include_str!("../tests/fixtures/Playground.artifact.json");
328
329    fn setup() -> (Layout, Getters) {
330        let v: serde_json::Value = serde_json::from_str(ARTIFACT).unwrap();
331        (
332            Layout::from_json(ARTIFACT).unwrap(),
333            Getters::from_artifact_json(&v),
334        )
335    }
336
337    fn call(sig: &str, args: &[[u8; 32]]) -> Vec<u8> {
338        let h = keccak256(sig.as_bytes());
339        let mut d = h[..4].to_vec();
340        for a in args {
341            d.extend_from_slice(a);
342        }
343        d
344    }
345
346    #[test]
347    fn value_mapping_nested_array_and_struct_getters_resolve() {
348        let (l, g) = setup();
349        assert_eq!(g.len(), 9);
350        let user: Address = "0x35825972e2ca90851b14576C531F13dA0B5d53ce"
351            .parse()
352            .unwrap();
353        let ukey = B256::left_padding_from(user.as_slice()).0;
354        let seven = U256::from(7).to_be_bytes::<32>();
355
356        let r = l
357            .resolve_call(&g, &call("counter()", &[]))
358            .unwrap()
359            .unwrap();
360        assert_eq!(
361            (r.path.as_str(), r.outputs.as_slice()),
362            ("counter", &["uint256".to_string()][..])
363        );
364        assert_eq!(r.reads[0], l.locate("counter").unwrap());
365
366        let r = l
367            .resolve_call(&g, &call("balances(address)", &[ukey]))
368            .unwrap()
369            .unwrap();
370        assert_eq!(r.reads[0], l.locate(&format!("balances[{user}]")).unwrap());
371
372        let r = l
373            .resolve_call(&g, &call("nested(address,uint256)", &[ukey, seven]))
374            .unwrap()
375            .unwrap();
376        assert_eq!(r.reads[0], l.locate(&format!("nested[{user}][7]")).unwrap());
377
378        let r = l
379            .resolve_call(&g, &call("items(uint256)", &[seven]))
380            .unwrap()
381            .unwrap();
382        assert_eq!(r.reads[0], l.locate("items[7]").unwrap());
383
384        // Struct getter: the members as a tuple, packed word shared.
385        let r = l.resolve_call(&g, &call("totals()", &[])).unwrap().unwrap();
386        assert_eq!(r.outputs, vec!["uint64".to_string(), "uint192".to_string()]);
387        assert_eq!(r.reads.len(), 2);
388        assert_eq!(r.reads[0], l.locate("totals.lastTime").unwrap());
389        assert_eq!(r.reads[1], l.locate("totals.index").unwrap());
390    }
391
392    #[test]
393    fn non_getters_are_not_resolved() {
394        let (l, g) = setup();
395        // Unknown selector: not ours.
396        assert_eq!(
397            l.resolve_call(&g, &call("getReserves()", &[])).unwrap(),
398            None
399        );
400        // Wrong argument count for a known selector: malformed, not guessed.
401        assert!(l.resolve_call(&g, &call("balances(address)", &[])).is_err());
402        // A view function that exists in the ABI but is not a variable.
403        let mut abi: serde_json::Value = serde_json::from_str(ARTIFACT).unwrap();
404        abi["abi"].as_array_mut().unwrap().push(serde_json::json!({
405            "type": "function", "name": "counterPlusOne", "stateMutability": "view",
406            "inputs": [], "outputs": [{ "type": "uint256" }]
407        }));
408        let g2 = Getters::from_artifact_json(&abi);
409        assert_eq!(
410            l.resolve_call(&g2, &call("counterPlusOne()", &[])).unwrap(),
411            None
412        );
413        // Same name as a variable but the wrong output type: not resolved.
414        abi["abi"].as_array_mut().unwrap().push(serde_json::json!({
415            "type": "function", "name": "lastPoker", "stateMutability": "view",
416            "inputs": [{ "type": "uint256" }], "outputs": [{ "type": "address" }]
417        }));
418        let g3 = Getters::from_artifact_json(&abi);
419        assert_eq!(
420            l.resolve_call(
421                &g3,
422                &call("lastPoker(uint256)", &[U256::ZERO.to_be_bytes::<32>()])
423            )
424            .unwrap(),
425            None
426        );
427    }
428
429    #[test]
430    fn return_encoding_static_and_dynamic() {
431        let user: Address = "0x35825972e2ca90851b14576C531F13dA0B5d53ce"
432            .parse()
433            .unwrap();
434        let out = encode_return(
435            &[
436                Value::Uint(U256::from(5)),
437                Value::Bool(true),
438                Value::Address(user),
439            ],
440            &["uint64".into(), "bool".into(), "address".into()],
441        )
442        .unwrap();
443        assert_eq!(out.len(), 96);
444        assert_eq!(out[31], 5);
445        assert_eq!(out[63], 1);
446        assert_eq!(&out[76..96], user.as_slice());
447        // A string: offset 32, length, data padded to a word.
448        let out = encode_return(&[Value::Str("hi".into())], &["string".into()]).unwrap();
449        assert_eq!(out.len(), 96);
450        assert_eq!(out[31], 32);
451        assert_eq!(out[63], 2);
452        assert_eq!(&out[64..66], b"hi");
453        // Negative int keeps its two's complement.
454        let out = encode_return(
455            &[Value::Int(I256::try_from(-1i64).unwrap())],
456            &["int128".into()],
457        )
458        .unwrap();
459        assert!(out.iter().all(|b| *b == 0xff));
460    }
461}