Skip to content
131 changes: 129 additions & 2 deletions crates/forge/src/cmd/inspect.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param};
use alloy_primitives::{hex, keccak256};
use alloy_primitives::{U256, hex, keccak256};
use clap::Parser;
use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS};
use eyre::{Result, eyre};
Expand Down Expand Up @@ -108,7 +108,9 @@ impl InspectArgs {
print_json(&artifact.gas_estimates)?;
}
ContractArtifactField::StorageLayout => {
print_storage_layout(artifact.storage_layout.as_ref(), wrap)?;
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 => {
print_json(&artifact.devdoc)?;
Expand Down Expand Up @@ -281,6 +283,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 {
Expand Down Expand Up @@ -314,6 +317,16 @@ pub fn print_storage_layout(
&slot.contract,
]);
}
for (type_str, slot_dec) in &bucket_rows {
table.add_row([
"storage-bucket",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be storage-location right? And can you provide some example output:)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Catch,

Sample Outputs should be in a comment with the main thread

type_str.as_str(),
slot_dec.as_str(),
"0",
"32",
type_str,
]);
}
},
should_wrap,
)
Expand Down Expand Up @@ -608,6 +621,65 @@ fn missing_error(field: &str) -> eyre::Error {
)
}

static BUCKET_PAIR_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?ix)
(?P<name>[A-Za-z_][A-Za-z0-9_:\.\-]*)
\s+
(?:0x)?(?P<hex>[0-9a-f]{1,64})
",
)
.unwrap()
});

fn parse_storage_buckets_value(raw_metadata: Option<&String>) -> Option<Vec<(String, String)>> {
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::<Vec<(String, String)>>()
};

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<Item=&str>
.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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should short this, is that a big issue if we display it entirely?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it just distorts the table render.

This is the difference in outputs

tc@TCs-MacBook-Pro frxAccount-EIP7702 % /Users/tc/Documents/GitHub/foundry/target/debug/forge inspect src/FrxCommerce.sol:FrxCommerceAccount storageLayout

╭----------------+----------------------+---------------+--------+-------+---------------╮
| Name           | Type                 | Slot          | Offset | Bytes | Contract      |
+========================================================================================+
| storage-bucket | struct EIP712Storage | 0xa16a46…d100 | 0      | 32    | EIP712Storage |
|----------------+----------------------+---------------+--------+-------+---------------|
| storage-bucket | struct NoncesStorage | 0x5ab42c…bb00 | 0      | 32    | NoncesStorage |
╰----------------+----------------------+---------------+--------+-------+---------------╯

tc@TCs-MacBook-Pro frxAccount-EIP7702 % /Users/tc/Documents/GitHub/foundry/target/debug/forge inspect src/FrxCommerce.sol:FrxCommerceAccount storageLayout

╭----------------+----------------------+--------------------------------------------------------------------+--------+-------+---------------╮
| Name           | Type                 | Slot                                                               | Offset | Bytes | Contract      |
+=============================================================================================================================================+
| storage-bucket | struct EIP712Storage | 0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100 | 0      | 32    | EIP712Storage |
|----------------+----------------------+--------------------------------------------------------------------+--------+-------+---------------|
| storage-bucket | struct NoncesStorage | 0x5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb00 | 0      | 32    | NoncesStorage |
╰----------------+----------------------+--------------------------------------------------------------------+--------+-------+---------------╯

Personally prefer the former but can change if you feel strongly

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, then maybe we could reuse

fn trimmed_hex(s: &[u8]) -> String {

@DaniPopes @zerosnacks wdyt?

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::*;
Expand Down Expand Up @@ -636,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('…'));
}
}
Loading