diff --git a/crates/cast/bin/cmd/storage.rs b/crates/cast/bin/cmd/storage.rs index 9fca4172e345..5e2459127001 100644 --- a/crates/cast/bin/cmd/storage.rs +++ b/crates/cast/bin/cmd/storage.rs @@ -17,6 +17,7 @@ use foundry_common::{ abi::find_source, compile::{etherscan_project, ProjectCompiler}, ens::NameOrAddress, + shell, }; use foundry_compilers::{ artifacts::{ConfigurableContractArtifact, StorageLayout}, @@ -31,6 +32,7 @@ use foundry_config::{ impl_figment_convert_cast, Config, }; use semver::Version; +use serde::{Deserialize, Serialize}; use std::str::FromStr; /// The minimum Solc version for outputting storage layouts. @@ -45,7 +47,7 @@ pub struct StorageArgs { #[arg(value_parser = NameOrAddress::from_str)] address: NameOrAddress, - /// The storage slot number. + /// The storage slot number. If not provided, it gets the full storage layout. #[arg(value_parser = parse_slot)] slot: Option, @@ -109,19 +111,22 @@ impl StorageArgs { if project.paths.has_input_files() { // Find in artifacts and pretty print add_storage_layout_output(&mut project); - let out = ProjectCompiler::new().compile(&project)?; + let out = ProjectCompiler::new().quiet(shell::is_json()).compile(&project)?; let artifact = out.artifacts().find(|(_, artifact)| { artifact.get_deployed_bytecode_bytes().is_some_and(|b| *b == address_code) }); if let Some((_, artifact)) = artifact { - return fetch_and_print_storage(provider, address, block, artifact, true).await; + return fetch_and_print_storage( + provider, + address, + block, + artifact, + !shell::is_json(), + ) + .await; } } - // Not a forge project or artifact not found - // Get code from Etherscan - sh_warn!("No matching artifacts found, fetching source code from Etherscan...")?; - if !self.etherscan.has_key() { eyre::bail!("You must provide an Etherscan API key if you're fetching a remote contract's storage."); } @@ -180,7 +185,7 @@ impl StorageArgs { // Clear temp directory root.close()?; - fetch_and_print_storage(provider, address, block, artifact, true).await + fetch_and_print_storage(provider, address, block, artifact, !shell::is_json()).await } } @@ -215,6 +220,14 @@ impl StorageValue { } } +/// Represents the storage layout of a contract and its values. +#[derive(Clone, Debug, Serialize, Deserialize)] +struct StorageReport { + #[serde(flatten)] + layout: StorageLayout, + values: Vec, +} + async fn fetch_and_print_storage, T: Transport + Clone>( provider: P, address: Address, @@ -255,7 +268,22 @@ async fn fetch_storage_slots, T: Transport + Clone>( fn print_storage(layout: StorageLayout, values: Vec, pretty: bool) -> Result<()> { if !pretty { - sh_println!("{}", serde_json::to_string_pretty(&serde_json::to_value(layout)?)?)?; + let values: Vec<_> = layout + .storage + .iter() + .zip(&values) + .map(|(slot, storage_value)| { + let storage_type = layout.types.get(&slot.storage_type); + storage_value.value( + slot.offset, + storage_type.and_then(|t| t.number_of_bytes.parse::().ok()), + ) + }) + .collect(); + sh_println!( + "{}", + serde_json::to_string_pretty(&serde_json::to_value(StorageReport { layout, values })?)? + )?; return Ok(()) } diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index a88369e97ffe..2483fa479820 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1131,6 +1131,23 @@ casttest!(storage_layout_simple, |_prj, cmd| { "#]]); }); +// +casttest!(storage_layout_simple_json, |_prj, cmd| { + cmd.args([ + "storage", + "--rpc-url", + next_rpc_endpoint(NamedChain::Mainnet).as_str(), + "--block", + "21034138", + "--etherscan-api-key", + next_mainnet_etherscan_api_key().as_str(), + "0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2", + "--json", + ]) + .assert_success() + .stdout_eq(file!["../fixtures/storage_layout_simple.json": Json]); +}); + // casttest!(storage_layout_complex, |_prj, cmd| { cmd.args([ @@ -1164,6 +1181,22 @@ casttest!(storage_layout_complex, |_prj, cmd| { "#]]); }); +casttest!(storage_layout_complex_json, |_prj, cmd| { + cmd.args([ + "storage", + "--rpc-url", + next_rpc_endpoint(NamedChain::Mainnet).as_str(), + "--block", + "21034138", + "--etherscan-api-key", + next_mainnet_etherscan_api_key().as_str(), + "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "--json", + ]) + .assert_success() + .stdout_eq(file!["../fixtures/storage_layout_complex.json": Json]); +}); + casttest!(balance, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); let usdt = "0xdac17f958d2ee523a2206206994597c13d831ec7"; diff --git a/crates/cast/tests/fixtures/storage_layout_complex.json b/crates/cast/tests/fixtures/storage_layout_complex.json new file mode 100644 index 000000000000..2cad9dc8c221 --- /dev/null +++ b/crates/cast/tests/fixtures/storage_layout_complex.json @@ -0,0 +1,397 @@ +{ + "storage": [ + { + "astId": 3805, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_status", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 9499, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_generalPoolsBalances", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_struct(IERC20ToBytes32Map)3177_storage)" + }, + { + "astId": 716, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_nextNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 967, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_paused", + "offset": 0, + "slot": "3", + "type": "t_bool" + }, + { + "astId": 8639, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_authorizer", + "offset": 1, + "slot": "3", + "type": "t_contract(IAuthorizer)11086" + }, + { + "astId": 8645, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_approvedRelayers", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))" + }, + { + "astId": 5769, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_isPoolRegistered", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_bytes32,t_bool)" + }, + { + "astId": 5771, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_nextPoolNonce", + "offset": 0, + "slot": "6", + "type": "t_uint256" + }, + { + "astId": 9915, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_minimalSwapInfoPoolsBalances", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_bytes32,t_mapping(t_contract(IERC20)3793,t_bytes32))" + }, + { + "astId": 9919, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_minimalSwapInfoPoolsTokens", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_bytes32,t_struct(AddressSet)3520_storage)" + }, + { + "astId": 10373, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_twoTokenPoolTokens", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_bytes32,t_struct(TwoTokenPoolTokens)10369_storage)" + }, + { + "astId": 4007, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_poolAssetManagers", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_bytes32,t_mapping(t_contract(IERC20)3793,t_address))" + }, + { + "astId": 8019, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_internalTokenBalance", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_address,t_mapping(t_contract(IERC20)3793,t_uint256))" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "encoding": "dynamic_array", + "label": "address[]", + "numberOfBytes": "32", + "base": "t_address" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IAuthorizer)11086": { + "encoding": "inplace", + "label": "contract IAuthorizer", + "numberOfBytes": "20" + }, + "t_contract(IERC20)3793": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_address,t_mapping(t_contract(IERC20)3793,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(contract IERC20 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)3793,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_bytes32,t_bool)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_bytes32,t_mapping(t_contract(IERC20)3793,t_address))": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => mapping(contract IERC20 => address))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)3793,t_address)" + }, + "t_mapping(t_bytes32,t_mapping(t_contract(IERC20)3793,t_bytes32))": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => mapping(contract IERC20 => bytes32))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)3793,t_bytes32)" + }, + "t_mapping(t_bytes32,t_struct(AddressSet)3520_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct EnumerableSet.AddressSet)", + "numberOfBytes": "32", + "value": "t_struct(AddressSet)3520_storage" + }, + "t_mapping(t_bytes32,t_struct(IERC20ToBytes32Map)3177_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct EnumerableMap.IERC20ToBytes32Map)", + "numberOfBytes": "32", + "value": "t_struct(IERC20ToBytes32Map)3177_storage" + }, + "t_mapping(t_bytes32,t_struct(TwoTokenPoolBalances)10360_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct TwoTokenPoolsBalance.TwoTokenPoolBalances)", + "numberOfBytes": "32", + "value": "t_struct(TwoTokenPoolBalances)10360_storage" + }, + "t_mapping(t_bytes32,t_struct(TwoTokenPoolTokens)10369_storage)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => struct TwoTokenPoolsBalance.TwoTokenPoolTokens)", + "numberOfBytes": "32", + "value": "t_struct(TwoTokenPoolTokens)10369_storage" + }, + "t_mapping(t_contract(IERC20)3793,t_address)": { + "encoding": "mapping", + "key": "t_contract(IERC20)3793", + "label": "mapping(contract IERC20 => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_contract(IERC20)3793,t_bytes32)": { + "encoding": "mapping", + "key": "t_contract(IERC20)3793", + "label": "mapping(contract IERC20 => bytes32)", + "numberOfBytes": "32", + "value": "t_bytes32" + }, + "t_mapping(t_contract(IERC20)3793,t_uint256)": { + "encoding": "mapping", + "key": "t_contract(IERC20)3793", + "label": "mapping(contract IERC20 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_struct(IERC20ToBytes32MapEntry)3166_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct EnumerableMap.IERC20ToBytes32MapEntry)", + "numberOfBytes": "32", + "value": "t_struct(IERC20ToBytes32MapEntry)3166_storage" + }, + "t_struct(AddressSet)3520_storage": { + "encoding": "inplace", + "label": "struct EnumerableSet.AddressSet", + "numberOfBytes": "64", + "members": [ + { + "astId": 3515, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_values", + "offset": 0, + "slot": "0", + "type": "t_array(t_address)dyn_storage" + }, + { + "astId": 3519, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_indexes", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_uint256)" + } + ] + }, + "t_struct(IERC20ToBytes32Map)3177_storage": { + "encoding": "inplace", + "label": "struct EnumerableMap.IERC20ToBytes32Map", + "numberOfBytes": "96", + "members": [ + { + "astId": 3168, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_length", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 3172, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_entries", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_uint256,t_struct(IERC20ToBytes32MapEntry)3166_storage)" + }, + { + "astId": 3176, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_indexes", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_contract(IERC20)3793,t_uint256)" + } + ] + }, + "t_struct(IERC20ToBytes32MapEntry)3166_storage": { + "encoding": "inplace", + "label": "struct EnumerableMap.IERC20ToBytes32MapEntry", + "numberOfBytes": "64", + "members": [ + { + "astId": 3163, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_key", + "offset": 0, + "slot": "0", + "type": "t_contract(IERC20)3793" + }, + { + "astId": 3165, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "_value", + "offset": 0, + "slot": "1", + "type": "t_bytes32" + } + ] + }, + "t_struct(TwoTokenPoolBalances)10360_storage": { + "encoding": "inplace", + "label": "struct TwoTokenPoolsBalance.TwoTokenPoolBalances", + "numberOfBytes": "64", + "members": [ + { + "astId": 10357, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "sharedCash", + "offset": 0, + "slot": "0", + "type": "t_bytes32" + }, + { + "astId": 10359, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "sharedManaged", + "offset": 0, + "slot": "1", + "type": "t_bytes32" + } + ] + }, + "t_struct(TwoTokenPoolTokens)10369_storage": { + "encoding": "inplace", + "label": "struct TwoTokenPoolsBalance.TwoTokenPoolTokens", + "numberOfBytes": "96", + "members": [ + { + "astId": 10362, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "tokenA", + "offset": 0, + "slot": "0", + "type": "t_contract(IERC20)3793" + }, + { + "astId": 10364, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "tokenB", + "offset": 0, + "slot": "1", + "type": "t_contract(IERC20)3793" + }, + { + "astId": 10368, + "contract": "contracts/vault/Vault.sol:Vault", + "label": "balances", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_bytes32,t_struct(TwoTokenPoolBalances)10360_storage)" + } + ] + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + }, + "values": [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000006048a8c631fb7e77eca533cf9c29784e482391e7", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000000000000000000000000000000000000000006e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file diff --git a/crates/cast/tests/fixtures/storage_layout_simple.json b/crates/cast/tests/fixtures/storage_layout_simple.json new file mode 100644 index 000000000000..35f4777d02b3 --- /dev/null +++ b/crates/cast/tests/fixtures/storage_layout_simple.json @@ -0,0 +1,36 @@ +{ + "storage": [ + { + "astId": 7, + "contract": "contracts/Create2Deployer.sol:Create2Deployer", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 122, + "contract": "contracts/Create2Deployer.sol:Create2Deployer", + "label": "_paused", + "offset": 20, + "slot": "0", + "type": "t_bool" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + } + }, + "values": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file