From d725fe407831dfc33f250ea2b2b1138584b04eeb Mon Sep 17 00:00:00 2001 From: Thomas Clement Date: Tue, 26 Aug 2025 11:38:10 -0400 Subject: [PATCH 1/6] Schema for 7201 buckets --- crates/forge/src/cmd/inspect.rs | 115 ++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 7783905c94279..5c82b273037b1 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -1,5 +1,5 @@ use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param}; -use alloy_primitives::{hex, keccak256}; +use alloy_primitives::{hex, keccak256, U256}; use clap::Parser; use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS}; use eyre::{Result, eyre}; @@ -108,7 +108,24 @@ impl InspectArgs { print_json(&artifact.gas_estimates)?; } ContractArtifactField::StorageLayout => { - print_storage_layout(artifact.storage_layout.as_ref(), wrap)?; + let mut bucket_rows: Vec<(String, String)> = Vec::new(); + if let Some(raw) = artifact.raw_metadata.as_ref() { + if let Ok(v) = serde_json::from_str::(raw) { + if let Some(constructor) = v + .get("output") + .and_then(|o| o.get("devdoc")) + .and_then(|d| d.get("methods")) + .and_then(|m| m.get("constructor")) + { + if let Some(obj) = constructor.as_object() { + if let Some(val) = obj.get("custom:storage-bucket") { + bucket_rows = parse_storage_buckets_value(val); + } + } + } + } + } + print_storage_layout(artifact.storage_layout.as_ref(), bucket_rows, wrap)?; } ContractArtifactField::DevDoc => { print_json(&artifact.devdoc)?; @@ -281,6 +298,7 @@ fn internal_ty(ty: &InternalType) -> String { pub fn print_storage_layout( storage_layout: Option<&StorageLayout>, + bucket_rows: Vec<(String, String)>, should_wrap: bool, ) -> Result<()> { let Some(storage_layout) = storage_layout else { @@ -300,23 +318,29 @@ pub fn print_storage_layout( Cell::new("Contract"), ]; - print_table( - headers, - |table| { - for slot in &storage_layout.storage { - let storage_type = storage_layout.types.get(&slot.storage_type); - table.add_row([ - slot.label.as_str(), - storage_type.map_or("?", |t| &t.label), - &slot.slot, - &slot.offset.to_string(), - storage_type.map_or("?", |t| &t.number_of_bytes), - &slot.contract, - ]); - } - }, - should_wrap, - ) + print_table(headers, |table| { + for slot in &storage_layout.storage { + let storage_type = storage_layout.types.get(&slot.storage_type); + table.add_row([ + slot.label.as_str(), + storage_type.map_or("?", |t| &t.label), + &slot.slot, + &slot.offset.to_string(), + storage_type.map_or("?", |t| &t.number_of_bytes), + &slot.contract, + ]); + } + for (type_str, slot_dec) in &bucket_rows { + table.add_row([ + "storage-bucket", + type_str.as_str(), + slot_dec.as_str(), + "0", + "32", + type_str.strip_prefix("struct ").unwrap_or(type_str.as_str()), + ]); + } + }, should_wrap) } fn print_method_identifiers( @@ -608,6 +632,59 @@ fn missing_error(field: &str) -> eyre::Error { ) } +fn parse_bucket_pairs_from_str(s: &str) -> Vec<(String, String)> { + static BUCKET_PAIR_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?ix) + (?P[A-Za-z_][A-Za-z0-9_:\.\-]*) + \s+ + (?:0x)?(?P[0-9a-f]{1,64}) + ").unwrap() + }); + BUCKET_PAIR_RE.captures_iter(s) + .filter_map(|cap| { + Some(( + cap.get(1)?.as_str().to_string(), // name -> String + cap.get(2)?.as_str().to_string(), // 0x.. -> String + )) + }) + .collect() +} + +fn parse_storage_buckets_value(v: &serde_json::Value) -> Vec<(String, String)> { + let mut pairs: Vec<(String, String)> = Vec::new(); + + match v { + serde_json::Value::String(s) => pairs.extend(parse_bucket_pairs_from_str(s)), + serde_json::Value::Array(arr) => { + for item in arr { + if let Some(s) = item.as_str() { + pairs.extend(parse_bucket_pairs_from_str(s)); + } + } + } + _ => {} + } + + pairs + .into_iter() + .filter_map(|(name, hex)| { + let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); + let slot = U256::from_str_radix(hex_str, 16).ok()?; + let slot_hex = short_hex(&alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>())); + Some((format!("struct {}", name), slot_hex)) + }) + .collect() +} + +fn short_hex(h: &str) -> String { + let s = h.strip_prefix("0x").unwrap_or(h); + if s.len() > 12 { + format!("0x{}…{}", &s[..6], &s[s.len()-4..]) + } else { + format!("0x{s}") + } +} + #[cfg(test)] mod tests { use super::*; From ba0031a5cdf2d8b62174e231ba5af4ef3d4fa1ee Mon Sep 17 00:00:00 2001 From: Thomas Clement Date: Wed, 27 Aug 2025 06:52:31 -0400 Subject: [PATCH 2/6] Clippy ++ fmt --- crates/forge/src/cmd/inspect.rs | 114 +++++++++++++++++--------------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 5c82b273037b1..2565c70d600b0 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -1,5 +1,5 @@ use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param}; -use alloy_primitives::{hex, keccak256, U256}; +use alloy_primitives::{U256, hex, keccak256}; use clap::Parser; use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS}; use eyre::{Result, eyre}; @@ -109,21 +109,17 @@ impl InspectArgs { } ContractArtifactField::StorageLayout => { let mut bucket_rows: Vec<(String, String)> = Vec::new(); - if let Some(raw) = artifact.raw_metadata.as_ref() { - if let Ok(v) = serde_json::from_str::(raw) { - if let Some(constructor) = v - .get("output") - .and_then(|o| o.get("devdoc")) - .and_then(|d| d.get("methods")) - .and_then(|m| m.get("constructor")) - { - if let Some(obj) = constructor.as_object() { - if let Some(val) = obj.get("custom:storage-bucket") { - bucket_rows = parse_storage_buckets_value(val); - } - } - } - } + if let Some(raw) = artifact.raw_metadata.as_ref() + && let Ok(v) = serde_json::from_str::(raw) + && let Some(constructor) = v + .get("output") + .and_then(|o| o.get("devdoc")) + .and_then(|d| d.get("methods")) + .and_then(|m| m.get("constructor")) + && let Some(obj) = constructor.as_object() + && let Some(val) = obj.get("custom:storage-bucket") + { + bucket_rows = parse_storage_buckets_value(val); } print_storage_layout(artifact.storage_layout.as_ref(), bucket_rows, wrap)?; } @@ -318,29 +314,33 @@ pub fn print_storage_layout( Cell::new("Contract"), ]; - print_table(headers, |table| { - for slot in &storage_layout.storage { - let storage_type = storage_layout.types.get(&slot.storage_type); - table.add_row([ - slot.label.as_str(), - storage_type.map_or("?", |t| &t.label), - &slot.slot, - &slot.offset.to_string(), - storage_type.map_or("?", |t| &t.number_of_bytes), - &slot.contract, - ]); - } - for (type_str, slot_dec) in &bucket_rows { - table.add_row([ - "storage-bucket", - type_str.as_str(), - slot_dec.as_str(), - "0", - "32", - type_str.strip_prefix("struct ").unwrap_or(type_str.as_str()), - ]); - } - }, should_wrap) + print_table( + headers, + |table| { + for slot in &storage_layout.storage { + let storage_type = storage_layout.types.get(&slot.storage_type); + table.add_row([ + slot.label.as_str(), + storage_type.map_or("?", |t| &t.label), + &slot.slot, + &slot.offset.to_string(), + storage_type.map_or("?", |t| &t.number_of_bytes), + &slot.contract, + ]); + } + for (type_str, slot_dec) in &bucket_rows { + table.add_row([ + "storage-bucket", + type_str.as_str(), + slot_dec.as_str(), + "0", + "32", + type_str.strip_prefix("struct ").unwrap_or(type_str.as_str()), + ]); + } + }, + should_wrap, + ) } fn print_method_identifiers( @@ -634,18 +634,29 @@ fn missing_error(field: &str) -> eyre::Error { fn parse_bucket_pairs_from_str(s: &str) -> Vec<(String, String)> { static BUCKET_PAIR_RE: LazyLock = LazyLock::new(|| { - Regex::new(r"(?ix) + Regex::new( + r"(?ix) (?P[A-Za-z_][A-Za-z0-9_:\.\-]*) \s+ (?:0x)?(?P[0-9a-f]{1,64}) - ").unwrap() + ", + ) + .unwrap() }); - BUCKET_PAIR_RE.captures_iter(s) + BUCKET_PAIR_RE + .captures_iter(s) .filter_map(|cap| { - Some(( - cap.get(1)?.as_str().to_string(), // name -> String - cap.get(2)?.as_str().to_string(), // 0x.. -> String - )) + let name = cap.get(1)?.as_str().to_string(); + let hex = cap.get(2)?.as_str().to_string(); + + // strip 0x and check decoded length + if let Ok(bytes) = hex::decode(hex.trim_start_matches("0x")) + && bytes.len() == 32 + { + return Some((name, hex)); + } + + None }) .collect() } @@ -670,19 +681,16 @@ fn parse_storage_buckets_value(v: &serde_json::Value) -> Vec<(String, String)> { .filter_map(|(name, hex)| { let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); let slot = U256::from_str_radix(hex_str, 16).ok()?; - let slot_hex = short_hex(&alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>())); - Some((format!("struct {}", name), slot_hex)) + let slot_hex = + short_hex(&alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>())); + Some((format!("struct {name}"), slot_hex)) }) .collect() } fn short_hex(h: &str) -> String { let s = h.strip_prefix("0x").unwrap_or(h); - if s.len() > 12 { - format!("0x{}…{}", &s[..6], &s[s.len()-4..]) - } else { - format!("0x{s}") - } + if s.len() > 12 { format!("0x{}…{}", &s[..6], &s[s.len() - 4..]) } else { format!("0x{s}") } } #[cfg(test)] From 586b827d3fbfffea18845942a4fe3d7dc2bc59ac Mon Sep 17 00:00:00 2001 From: Thomas Clement Date: Thu, 28 Aug 2025 12:50:07 -0400 Subject: [PATCH 3/6] Edits @grandizzy --- crates/forge/src/cmd/inspect.rs | 174 ++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 66 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 2565c70d600b0..6a26b846dbd49 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -108,19 +108,7 @@ impl InspectArgs { print_json(&artifact.gas_estimates)?; } ContractArtifactField::StorageLayout => { - let mut bucket_rows: Vec<(String, String)> = Vec::new(); - if let Some(raw) = artifact.raw_metadata.as_ref() - && let Ok(v) = serde_json::from_str::(raw) - && let Some(constructor) = v - .get("output") - .and_then(|o| o.get("devdoc")) - .and_then(|d| d.get("methods")) - .and_then(|m| m.get("constructor")) - && let Some(obj) = constructor.as_object() - && let Some(val) = obj.get("custom:storage-bucket") - { - bucket_rows = parse_storage_buckets_value(val); - } + let bucket_rows = parse_storage_buckets_value(artifact.raw_metadata.as_ref()).unwrap_or_default(); print_storage_layout(artifact.storage_layout.as_ref(), bucket_rows, wrap)?; } ContractArtifactField::DevDoc => { @@ -335,7 +323,7 @@ pub fn print_storage_layout( slot_dec.as_str(), "0", "32", - type_str.strip_prefix("struct ").unwrap_or(type_str.as_str()), + type_str, ]); } }, @@ -632,60 +620,59 @@ fn missing_error(field: &str) -> eyre::Error { ) } -fn parse_bucket_pairs_from_str(s: &str) -> Vec<(String, String)> { - static BUCKET_PAIR_RE: LazyLock = LazyLock::new(|| { - Regex::new( - r"(?ix) - (?P[A-Za-z_][A-Za-z0-9_:\.\-]*) - \s+ - (?:0x)?(?P[0-9a-f]{1,64}) - ", - ) - .unwrap() - }); - BUCKET_PAIR_RE - .captures_iter(s) - .filter_map(|cap| { - let name = cap.get(1)?.as_str().to_string(); - let hex = cap.get(2)?.as_str().to_string(); - - // strip 0x and check decoded length - if let Ok(bytes) = hex::decode(hex.trim_start_matches("0x")) - && bytes.len() == 32 - { - return Some((name, hex)); - } - - None - }) - .collect() -} - -fn parse_storage_buckets_value(v: &serde_json::Value) -> Vec<(String, String)> { - let mut pairs: Vec<(String, String)> = Vec::new(); - - match v { - serde_json::Value::String(s) => pairs.extend(parse_bucket_pairs_from_str(s)), - serde_json::Value::Array(arr) => { - for item in arr { - if let Some(s) = item.as_str() { - pairs.extend(parse_bucket_pairs_from_str(s)); +static BUCKET_PAIR_RE: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?ix) + (?P[A-Za-z_][A-Za-z0-9_:\.\-]*) + \s+ + (?:0x)?(?P[0-9a-f]{1,64}) + ", + ) + .unwrap() +}); + +fn parse_storage_buckets_value(raw_metadata: Option<&String>) -> Option> { + let parse_bucket_pairs = |s: &str| { + BUCKET_PAIR_RE + .captures_iter(s) + .filter_map(|cap| { + let name = cap.get(1)?.as_str().to_string(); + let hex = cap.get(2)?.as_str().to_string(); + // strip 0x and check decoded length + if let Ok(bytes) = hex::decode(hex.trim_start_matches("0x")) + && bytes.len() == 32 + { + return Some((name, hex)); } - } - } - _ => {} - } + None + }) + .collect::>() + }; - pairs - .into_iter() - .filter_map(|(name, hex)| { - let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); - let slot = U256::from_str_radix(hex_str, 16).ok()?; - let slot_hex = - short_hex(&alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>())); - Some((format!("struct {name}"), slot_hex)) - }) - .collect() + let raw = raw_metadata?; + let v: serde_json::Value = serde_json::from_str(raw).ok()?; + let val = v + .get("output") + .and_then(|o| o.get("devdoc")) + .and_then(|d| d.get("methods")) + .and_then(|m| m.get("constructor")) + .and_then(|c| c.as_object()) + .and_then(|obj| obj.get("custom:storage-bucket"))?; + + Some( + val.as_str() + .into_iter() // Option<&str> → Iterator + .flat_map(parse_bucket_pairs) + .filter_map(|(name, hex): (String, String)| { + let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); + let slot = U256::from_str_radix(hex_str, 16).ok()?; + let slot_hex = short_hex( + &alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>()), + ); + Some((name, slot_hex)) + }) + .collect(), + ) } fn short_hex(h: &str) -> String { @@ -721,4 +708,59 @@ mod tests { } } } + + #[test] + fn parses_eip7201_storage_buckets_from_metadata() { + let raw_wrapped = r#" + { + "metadata": { + "compiler": { "version": "0.8.30+commit.73712a01" }, + "language": "Solidity", + "output": { + "abi": [], + "devdoc": { + "kind": "dev", + "methods": { + "constructor": { + "custom:storage-bucket": "EIP712Storage 0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100NoncesStorage 0x5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb00" + } + }, + "version": 1 + }, + "userdoc": { "kind": "user", "methods": {}, "version": 1 } + }, + "settings": { "optimizer": { "enabled": false, "runs": 200 } }, + "sources": {}, + "version": 1 + } + }"#; + + let v: serde_json::Value = serde_json::from_str(raw_wrapped).unwrap(); + let inner_meta_str = v.get("metadata").unwrap().to_string(); + + let rows = + parse_storage_buckets_value(Some(&inner_meta_str)).expect("parser returned None"); + assert_eq!(rows.len(), 2, "expected two EIP-7201 buckets"); + + assert_eq!(rows[0].0, "EIP712Storage"); + assert_eq!(rows[1].0, "NoncesStorage"); + + let expect_short = |h: &str| { + let hex_str = h.trim_start_matches("0x"); + let slot = U256::from_str_radix(hex_str, 16).unwrap(); + let full = alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>()); + short_hex(&full) + }; + + let eip712_slot_hex = + expect_short("0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100"); + let nonces_slot_hex = + expect_short("0x5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb00"); + + assert_eq!(rows[0].1, eip712_slot_hex); + assert_eq!(rows[1].1, nonces_slot_hex); + + assert!(rows[0].1.starts_with("0x") && rows[0].1.contains('…')); + assert!(rows[1].1.starts_with("0x") && rows[1].1.contains('…')); + } } From 0eb1f43928ba9b52f1604c25f672d667f3e53f9c Mon Sep 17 00:00:00 2001 From: Thomas Clement Date: Thu, 28 Aug 2025 13:24:59 -0400 Subject: [PATCH 4/6] Fix: cargo fmt --all --check --- crates/forge/src/cmd/inspect.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 6a26b846dbd49..96c4ecbbab000 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -108,7 +108,8 @@ impl InspectArgs { print_json(&artifact.gas_estimates)?; } ContractArtifactField::StorageLayout => { - let bucket_rows = parse_storage_buckets_value(artifact.raw_metadata.as_ref()).unwrap_or_default(); + let bucket_rows = + parse_storage_buckets_value(artifact.raw_metadata.as_ref()).unwrap_or_default(); print_storage_layout(artifact.storage_layout.as_ref(), bucket_rows, wrap)?; } ContractArtifactField::DevDoc => { @@ -666,9 +667,8 @@ fn parse_storage_buckets_value(raw_metadata: Option<&String>) -> Option()), - ); + let slot_hex = + short_hex(&alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>())); Some((name, slot_hex)) }) .collect(), From c761f2edbf5fb32c9cfd4d55b1dbfa34ec824d45 Mon Sep 17 00:00:00 2001 From: Thomas Clement Date: Fri, 29 Aug 2025 08:13:38 -0400 Subject: [PATCH 5/6] Edits less short_hex -> trimmed_hex --- crates/forge/src/cmd/inspect.rs | 54 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 96c4ecbbab000..023fa5b9fe77a 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -636,43 +636,39 @@ fn parse_storage_buckets_value(raw_metadata: Option<&String>) -> Option>() + .collect::>() }; - let raw = raw_metadata?; let v: serde_json::Value = serde_json::from_str(raw).ok()?; - let val = v - .get("output") + v.get("output") .and_then(|o| o.get("devdoc")) .and_then(|d| d.get("methods")) .and_then(|m| m.get("constructor")) .and_then(|c| c.as_object()) - .and_then(|obj| obj.get("custom:storage-bucket"))?; - - Some( - val.as_str() - .into_iter() // Option<&str> → Iterator - .flat_map(parse_bucket_pairs) - .filter_map(|(name, hex): (String, String)| { - let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); - let slot = U256::from_str_radix(hex_str, 16).ok()?; - let slot_hex = - short_hex(&alloy_primitives::hex::encode_prefixed(slot.to_be_bytes::<32>())); - Some((name, slot_hex)) - }) - .collect(), - ) + .and_then(|obj| obj.get("custom:storage-bucket")) + .map(|val| { + val.as_str() + .into_iter() // Option<&str> → Iterator + .flat_map(parse_bucket_pairs) + .filter_map(|(name, hex): (String, String)| { + let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); + let slot = U256::from_str_radix(hex_str, 16).ok()?; + let slot_hex = short_hex(&alloy_primitives::hex::encode_prefixed( + slot.to_be_bytes::<32>(), + )); + Some((name, slot_hex)) + }) + .collect() + }) } fn short_hex(h: &str) -> String { From e49cc6ddb0f19c9e2c304aa1e96b472d309a47eb Mon Sep 17 00:00:00 2001 From: Thomas Clement Date: Thu, 18 Sep 2025 11:10:49 -0400 Subject: [PATCH 6/6] Edits @onbjerg Use custom:storage-location and derive slot based on formula in EIP --- crates/forge/src/cmd/inspect.rs | 156 ++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 49 deletions(-) diff --git a/crates/forge/src/cmd/inspect.rs b/crates/forge/src/cmd/inspect.rs index 023fa5b9fe77a..523e37d9e8d2e 100644 --- a/crates/forge/src/cmd/inspect.rs +++ b/crates/forge/src/cmd/inspect.rs @@ -109,7 +109,7 @@ impl InspectArgs { } ContractArtifactField::StorageLayout => { let bucket_rows = - parse_storage_buckets_value(artifact.raw_metadata.as_ref()).unwrap_or_default(); + parse_storage_locations(artifact.raw_metadata.as_ref()).unwrap_or_default(); print_storage_layout(artifact.storage_layout.as_ref(), bucket_rows, wrap)?; } ContractArtifactField::DevDoc => { @@ -621,54 +621,113 @@ fn missing_error(field: &str) -> eyre::Error { ) } -static BUCKET_PAIR_RE: LazyLock = LazyLock::new(|| { +static STORAGE_LOC_HEAD_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)erc[0-9]+\s*:").unwrap()); + +static STORAGE_LOC_PAIR_RE: LazyLock = LazyLock::new(|| { Regex::new( - r"(?ix) - (?P[A-Za-z_][A-Za-z0-9_:\.\-]*) - \s+ - (?:0x)?(?P[0-9a-f]{1,64}) - ", + r"(?ix) ^ + (?Perc[0-9]+) # erc ID + \s*:\s* + (?P[A-Za-z0-9_.\-]+) # namespace (no colon) + $", ) .unwrap() }); -fn parse_storage_buckets_value(raw_metadata: Option<&String>) -> Option> { - let parse_bucket_pairs = |s: &str| { - BUCKET_PAIR_RE - .captures_iter(s) - .filter_map(|caps| { - let name = caps.get(1)?.as_str(); - let hex_str = caps.get(2)?.as_str(); - - hex::decode(hex_str.trim_start_matches("0x")) - .ok() - .filter(|bytes| bytes.len() == 32) - .map(|_| (name.to_owned(), hex_str.to_owned())) - }) - .collect::>() - }; - let raw = raw_metadata?; - let v: serde_json::Value = serde_json::from_str(raw).ok()?; - v.get("output") - .and_then(|o| o.get("devdoc")) - .and_then(|d| d.get("methods")) +fn split_erc_formulas(s: &str) -> Vec<(String, String)> { + let mut starts: Vec = STORAGE_LOC_HEAD_RE.find_iter(s).map(|m| m.start()).collect(); + + if starts.is_empty() { + return Vec::new(); + } + starts.push(s.len()); + let mut out = Vec::new(); + for w in starts.windows(2) { + let (beg, end) = (w[0], w[1]); + let slice = s[beg..end].trim(); + if let Some(caps) = STORAGE_LOC_PAIR_RE.captures(slice) { + let formula = caps.name("formula").unwrap().as_str().to_string(); + let ns = caps.name("ns").unwrap().as_str().to_string(); + out.push((formula, ns)); + } + } + out +} + +#[inline] +fn compute_erc7201_slot_hex(ns: &str) -> String { + // Step 1: keccak256(bytes(id)) + let ns_hash = keccak256(ns.as_bytes()); // 32 bytes + + // Step 2: (uint256(keccak256(id)) - 1) as 32-byte big-endian + let mut u = U256::from_be_slice(ns_hash.as_slice()); + u = u.wrapping_sub(U256::from(1u8)); + let enc = u.to_be_bytes::<32>(); + + // Step 3: keccak256(abi.encode(uint256(...))) + let slot_hash = keccak256(enc); + + // Step 4: & ~0xff (zero out the lowest byte) + let mut slot_u = U256::from_be_slice(slot_hash.as_slice()); + slot_u &= !U256::from(0xffu8); + + // 0x-prefixed 32-byte hex, optionally shorten with your helper + let full = hex::encode_prefixed(slot_u.to_be_bytes::<32>()); + short_hex(&full) +} + +// Simple “formula registry” so future EIPs can be added without touching the parser. +fn derive_slot_hex(formula: &str, ns: &str) -> Option { + match formula.to_ascii_lowercase().as_str() { + "erc7201" => Some(compute_erc7201_slot_hex(ns)), + // For future EIPs: add "erc1234" => Some(compute_erc1234_slot_hex(ns)) + _ => None, + } +} + +fn strings_from_json(val: &serde_json::Value) -> Vec { + match val { + serde_json::Value::String(s) => vec![s.clone()], + serde_json::Value::Array(arr) => { + arr.iter().filter_map(|v| v.as_str().map(str::to_owned)).collect() + } + _ => vec![], + } +} + +fn get_custom_tag_lines(devdoc: &serde_json::Value, key: &str) -> Vec { + if let Some(v) = devdoc.get(key) { + let xs = strings_from_json(v); + if !xs.is_empty() { + return xs; + } + } + devdoc + .get("methods") .and_then(|m| m.get("constructor")) .and_then(|c| c.as_object()) - .and_then(|obj| obj.get("custom:storage-bucket")) - .map(|val| { - val.as_str() - .into_iter() // Option<&str> → Iterator - .flat_map(parse_bucket_pairs) - .filter_map(|(name, hex): (String, String)| { - let hex_str = hex.strip_prefix("0x").unwrap_or(&hex); - let slot = U256::from_str_radix(hex_str, 16).ok()?; - let slot_hex = short_hex(&alloy_primitives::hex::encode_prefixed( - slot.to_be_bytes::<32>(), - )); - Some((name, slot_hex)) - }) - .collect() - }) + .and_then(|obj| obj.get(key)) + .map(strings_from_json) + .unwrap_or_default() +} + +pub fn parse_storage_locations(raw_metadata: Option<&String>) -> Option> { + let raw = raw_metadata?; + let v: serde_json::Value = serde_json::from_str(raw).ok()?; + let devdoc = v.get("output")?.get("devdoc")?; + + let loc_lines = get_custom_tag_lines(devdoc, "custom:storage-location"); + let out: Vec<(String, String)> = loc_lines + .iter() + .flat_map(|s| split_erc_formulas(s)) + .filter_map(|(formula, ns)| derive_slot_hex(&formula, &ns).map(|slot_hex| (ns, slot_hex))) + .collect(); + + if !out.is_empty() { + return Some(out); + } + if !out.is_empty() { Some(out) } else { None } } fn short_hex(h: &str) -> String { @@ -718,7 +777,7 @@ mod tests { "kind": "dev", "methods": { "constructor": { - "custom:storage-bucket": "EIP712Storage 0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100NoncesStorage 0x5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb00" + "custom:storage-location": "erc7201:openzeppelin.storage.ERC20erc7201:openzeppelin.storage.AccessControlDefaultAdminRules" } }, "version": 1 @@ -734,12 +793,11 @@ mod tests { let v: serde_json::Value = serde_json::from_str(raw_wrapped).unwrap(); let inner_meta_str = v.get("metadata").unwrap().to_string(); - let rows = - parse_storage_buckets_value(Some(&inner_meta_str)).expect("parser returned None"); + let rows = parse_storage_locations(Some(&inner_meta_str)).expect("parser returned None"); assert_eq!(rows.len(), 2, "expected two EIP-7201 buckets"); - assert_eq!(rows[0].0, "EIP712Storage"); - assert_eq!(rows[1].0, "NoncesStorage"); + assert_eq!(rows[0].0, "openzeppelin.storage.ERC20"); + assert_eq!(rows[1].0, "openzeppelin.storage.AccessControlDefaultAdminRules"); let expect_short = |h: &str| { let hex_str = h.trim_start_matches("0x"); @@ -749,9 +807,9 @@ mod tests { }; let eip712_slot_hex = - expect_short("0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100"); + expect_short("0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00"); let nonces_slot_hex = - expect_short("0x5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb00"); + expect_short("0xeef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400"); assert_eq!(rows[0].1, eip712_slot_hex); assert_eq!(rows[1].1, nonces_slot_hex);