1use crate::{Encoding, Layout, LayoutError, Location, Result, StorageEntry, TypeInfo, Value};
10use alloy_primitives::{keccak256, Address, B256, U256};
11use std::path::Path;
12
13#[derive(Debug, serde::Deserialize)]
33#[serde(deny_unknown_fields)]
34struct Manifest {
35 base: Option<String>,
36 #[serde(default)]
37 namespaces: Vec<Namespace>,
38}
39
40#[derive(Debug, serde::Deserialize)]
41#[serde(deny_unknown_fields)]
42struct Namespace {
43 prefix: String,
44 layout: String,
45 erc7201: Option<String>,
46 slot: Option<String>,
47}
48
49impl Layout {
50 pub fn erc7201_slot(id: &str) -> B256 {
52 let h = U256::from_be_bytes(keccak256(id.as_bytes()).0).wrapping_sub(U256::from(1));
53 let mut out = keccak256(h.to_be_bytes::<32>()).0;
54 out[31] = 0;
55 B256::from(out)
56 }
57
58 pub fn mount(&mut self, prefix: &str, other: &Layout, base: B256) -> Result<()> {
62 if prefix.is_empty() || prefix.contains(['.', '[', ']']) {
63 return Err(LayoutError::Syntax(prefix.into()));
64 }
65 if self.storage.iter().any(|e| e.label == prefix) {
66 return Err(LayoutError::Syntax(format!("{prefix}: name already used")));
67 }
68 for (id, t) in &other.types {
69 self.types.entry(id.clone()).or_insert_with(|| t.clone());
70 }
71 let words = other
72 .storage
73 .iter()
74 .map(|e| {
75 let size = other
76 .types
77 .get(&e.type_id)
78 .map(|t| t.number_of_bytes)
79 .unwrap_or(32);
80 e.slot + U256::from(size.div_ceil(32))
81 })
82 .max()
83 .unwrap_or(U256::ZERO);
84 let type_id = format!("t_struct(namespace {prefix})_storage");
85 self.types.insert(
86 type_id.clone(),
87 TypeInfo {
88 encoding: Encoding::Inplace,
89 label: format!("struct {prefix}"),
90 number_of_bytes: words.saturating_to::<usize>().saturating_mul(32),
91 key: None,
92 value: None,
93 base: None,
94 members: Some(other.storage.clone()),
95 },
96 );
97 self.storage.push(StorageEntry {
98 label: prefix.to_string(),
99 slot: U256::from_be_bytes(base.0),
100 offset: 0,
101 type_id,
102 });
103 Ok(())
104 }
105
106 pub fn from_manifest(path: impl AsRef<Path>) -> Result<Self> {
109 let path = path.as_ref();
110 let text = std::fs::read_to_string(path)?;
111 let m: Manifest =
112 serde_json::from_str(&text).map_err(|e| LayoutError::Json(e.to_string()))?;
113 let dir = path.parent().unwrap_or(Path::new("."));
114 let mut layout = match &m.base {
115 Some(b) => Layout::from_artifact(dir.join(b))?,
116 None => Layout::from_json(r#"{"storage":[],"types":{}}"#)?,
117 };
118 for ns in &m.namespaces {
119 let other = Layout::from_artifact(dir.join(&ns.layout))?;
120 let base = match (&ns.erc7201, &ns.slot) {
121 (Some(id), None) => Layout::erc7201_slot(id),
122 (None, Some(s)) => s
123 .parse::<B256>()
124 .map_err(|_| LayoutError::Syntax(format!("{}: slot {s}", ns.prefix)))?,
125 _ => {
126 return Err(LayoutError::Syntax(format!(
127 "{}: exactly one of erc7201 / slot",
128 ns.prefix
129 )))
130 }
131 };
132 layout.mount(&ns.prefix, &other, base)?;
133 }
134 Ok(layout)
135 }
136
137 pub fn is_dynamic_bytes(&self, loc: &Location) -> bool {
139 self.types
140 .get(&loc.type_id)
141 .is_some_and(|t| t.encoding == Encoding::Bytes)
142 }
143
144 pub fn bytes_data_slots(&self, loc: &Location, word: B256) -> Vec<B256> {
148 if !self.is_dynamic_bytes(loc) || word.0[31] & 1 == 0 {
149 return Vec::new();
150 }
151 let len = (U256::from_be_bytes(word.0) - U256::from(1)) / U256::from(2);
152 let words = len
154 .div_ceil(U256::from(32))
155 .min(U256::from(4096))
156 .to::<u64>();
157 let data = U256::from_be_bytes(keccak256(loc.slot.as_slice()).0);
158 (0..words)
159 .map(|i| B256::from(data.wrapping_add(U256::from(i)).to_be_bytes::<32>()))
160 .collect()
161 }
162
163 pub fn decode_bytes(&self, loc: &Location, word: B256, chunks: &[B256]) -> Value {
167 if !self.is_dynamic_bytes(loc) {
168 return self.decode(loc, word);
169 }
170 let raw: Vec<u8> = if word.0[31] & 1 == 0 {
171 let len = (word.0[31] / 2) as usize;
172 word.0[..len.min(31)].to_vec()
173 } else {
174 let len = ((U256::from_be_bytes(word.0) - U256::from(1)) / U256::from(2))
175 .min(U256::from(chunks.len() * 32))
176 .to::<usize>();
177 let mut out = Vec::with_capacity(len);
178 for c in chunks {
179 out.extend_from_slice(c.as_slice());
180 }
181 out.truncate(len);
182 out
183 };
184 let is_string = self
185 .types
186 .get(&loc.type_id)
187 .is_some_and(|t| t.label == "string" || t.label == "string storage ref");
188 if is_string {
189 match String::from_utf8(raw) {
190 Ok(s) => Value::Str(s),
191 Err(e) => Value::Bytes(e.into_bytes()),
192 }
193 } else {
194 Value::Bytes(raw)
195 }
196 }
197
198 pub fn describe_slot_with_keys(
203 &self,
204 slot: B256,
205 array_probe: u64,
206 keys: &[B256],
207 ) -> Vec<(String, Location)> {
208 let mut out = self.describe_slot(slot, array_probe);
209 if keys.is_empty() {
210 return out;
211 }
212 let target = U256::from_be_bytes(slot.0);
213 for e in &self.storage {
214 self.describe_mappings_in(
215 &e.label,
216 e.slot,
217 &e.type_id,
218 target,
219 array_probe,
220 keys,
221 0,
222 &mut out,
223 );
224 }
225 out
226 }
227
228 #[allow(clippy::too_many_arguments)]
229 fn describe_mappings_in(
230 &self,
231 name: &str,
232 base: U256,
233 type_id: &str,
234 target: U256,
235 probe: u64,
236 keys: &[B256],
237 depth: usize,
238 out: &mut Vec<(String, Location)>,
239 ) {
240 if depth > 8 {
241 return;
242 }
243 let Ok(t) = self.ty(type_id) else { return };
244 match (t.encoding, t.members.as_deref()) {
245 (Encoding::Inplace, Some(members)) => {
246 for m in members {
247 self.describe_mappings_in(
248 &format!("{name}.{}", m.label),
249 base + m.slot,
250 &m.type_id,
251 target,
252 probe,
253 keys,
254 depth + 1,
255 out,
256 );
257 }
258 }
259 (Encoding::Mapping, _) => {
260 let (Some(kt), Some(vt)) = (t.key.as_deref(), t.value.as_deref()) else {
261 return;
262 };
263 let key_label = self.ty(kt).map(|k| k.label.clone()).unwrap_or_default();
264 for key in keys {
265 let mut buf = [0u8; 64];
266 buf[..32].copy_from_slice(key.as_slice());
267 buf[32..].copy_from_slice(&base.to_be_bytes::<32>());
268 let s1 = U256::from_be_bytes(keccak256(buf).0);
269 let entry = format!("{name}[{}]", show_key(key, &key_label));
270 let Ok(v) = self.ty(vt) else { continue };
271 match (v.encoding, v.members.is_some(), v.base.is_some()) {
272 (Encoding::Mapping, _, _) => {
273 self.describe_mappings_in(
274 &entry,
275 s1,
276 vt,
277 target,
278 probe,
279 keys,
280 depth + 1,
281 out,
282 );
283 }
284 (Encoding::Inplace, false, false) | (Encoding::Bytes, _, _) => {
285 if s1 == target {
286 out.push((
287 entry,
288 Location {
289 slot: slot_b(target),
290 offset: 0,
291 size: v.number_of_bytes,
292 type_id: vt.to_string(),
293 },
294 ));
295 }
296 }
297 _ => {
298 self.describe_in(&entry, s1, 0, vt, target, probe, out);
300 self.describe_mappings_in(
301 &entry,
302 s1,
303 vt,
304 target,
305 probe,
306 keys,
307 depth + 1,
308 out,
309 );
310 }
311 }
312 }
313 }
314 _ => {}
315 }
316 }
317}
318
319fn slot_b(u: U256) -> B256 {
320 B256::from(u.to_be_bytes::<32>())
321}
322
323fn show_key(key: &B256, key_label: &str) -> String {
326 if key_label.starts_with("address") || key_label.starts_with("contract ") {
327 Address::from_slice(&key.0[12..]).to_string()
328 } else {
329 U256::from_be_bytes(key.0).to_string()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 #![allow(clippy::unwrap_used)]
336 use super::*;
337
338 const FIXTURE: &str = include_str!("../tests/fixtures/Playground.layout.json");
339
340 #[test]
341 fn erc7201_matches_openzeppelin() {
342 let s = Layout::erc7201_slot("openzeppelin.storage.ERC20");
344 assert_eq!(
345 format!("{s}"),
346 "0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00"
347 );
348 assert_eq!(s.0[31], 0, "low byte cleared");
349 }
350
351 #[test]
352 fn mount_namespace_and_read_through_it() {
353 let mut base = Layout::from_json(r#"{"storage":[],"types":{}}"#).unwrap();
354 let ns = Layout::from_json(FIXTURE).unwrap();
355 let at = Layout::erc7201_slot("test.playground");
356 base.mount("pg", &ns, at).unwrap();
357 let counter = base.locate("pg.counter").unwrap();
358 assert_eq!(counter.slot, at);
359 let bal = base
360 .locate("pg.balances[0x35825972e2ca90851b14576C531F13dA0B5d53ce]")
361 .unwrap();
362 assert_ne!(bal.slot, at);
363 let names = base.describe_slot(counter.slot, 16);
365 assert!(names.iter().any(|(n, _)| n == "pg.counter"), "{names:?}");
366 assert!(
367 base.mount("pg", &ns, at).is_err(),
368 "duplicate prefix refused"
369 );
370 assert!(base.mount("a.b", &ns, at).is_err(), "dots refused");
371 assert!(base.typescript("V").contains("readonly pg: {"));
372 }
373
374 #[test]
375 fn mapping_slots_are_named_from_candidate_keys() {
376 let l = Layout::from_json(FIXTURE).unwrap();
377 let user: Address = "0x35825972e2ca90851b14576C531F13dA0B5d53ce"
378 .parse()
379 .unwrap();
380 let key = B256::left_padding_from(user.as_slice());
381 let loc = l.locate(&format!("balances[{user}]")).unwrap();
382 assert!(
383 l.describe_slot(loc.slot, 16).is_empty(),
384 "one-way without a key"
385 );
386 let named = l.describe_slot_with_keys(loc.slot, 16, &[key]);
387 assert_eq!(named.len(), 1);
388 assert_eq!(named[0].0, format!("balances[{user}]"));
389 let other = B256::left_padding_from(Address::repeat_byte(9).as_slice());
391 assert!(l.describe_slot_with_keys(loc.slot, 16, &[other]).is_empty());
392 }
393
394 #[test]
395 fn dynamic_bytes_short_and_long() {
396 let l = Layout::from_json(
398 r#"{"storage":[{"label":"name","slot":"0","offset":0,"type":"t_string_storage"}],
399 "types":{"t_string_storage":{"encoding":"bytes","label":"string","numberOfBytes":"32"}}}"#,
400 )
401 .unwrap();
402 let loc = l.locate("name").unwrap();
403 assert!(l.is_dynamic_bytes(&loc));
404 let mut w = [0u8; 32];
406 w[..2].copy_from_slice(b"hi");
407 w[31] = 4;
408 let word = B256::from(w);
409 assert!(l.bytes_data_slots(&loc, word).is_empty());
410 assert_eq!(l.decode_bytes(&loc, word, &[]), Value::Str("hi".into()));
411 let text = "0123456789012345678901234567890123456789";
413 let lenw = B256::from(U256::from(text.len() * 2 + 1).to_be_bytes::<32>());
414 let slots = l.bytes_data_slots(&loc, lenw);
415 assert_eq!(slots.len(), 2);
416 let data = U256::from_be_bytes(keccak256(loc.slot.as_slice()).0);
417 assert_eq!(slots[0], B256::from(data.to_be_bytes::<32>()));
418 let mut c0 = [0u8; 32];
419 c0.copy_from_slice(&text.as_bytes()[..32]);
420 let mut c1 = [0u8; 32];
421 c1[..8].copy_from_slice(&text.as_bytes()[32..]);
422 assert_eq!(
423 l.decode_bytes(&loc, lenw, &[B256::from(c0), B256::from(c1)]),
424 Value::Str(text.into())
425 );
426 }
427}