diff --git a/.github/workflows/live-test.yml b/.github/workflows/live-test.yml index 6dbd397e..06277371 100644 --- a/.github/workflows/live-test.yml +++ b/.github/workflows/live-test.yml @@ -18,16 +18,17 @@ jobs: TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }} steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true + - name: Install nextest + uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v1 with: cache-on-failure: true - - - name: cargo test - run: cargo test --package foundry-cli --test it -- verify::test_live_can_deploy_and_verify --exact --nocapture + - name: cargo nextest + run: cargo nextest run -p foundry-cli -E "!test(~fork) & test(~live)" diff --git a/Cargo.lock b/Cargo.lock index aa858224..a844d009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2335,6 +2335,7 @@ dependencies = [ "strsim", "strum", "svm-rs", + "tempfile", "thiserror", "tokio", "toml", diff --git a/cast/src/lib.rs b/cast/src/lib.rs index 33571c51..0a43b398 100644 --- a/cast/src/lib.rs +++ b/cast/src/lib.rs @@ -662,6 +662,35 @@ where let res = self.provider.provider().request::(method, params).await?; Ok(serde_json::to_string(&res)?) } + + /// Returns the slot + /// + /// # Example + /// + /// ```no_run + /// use cast::Cast; + /// use ethers_providers::{Provider, Http}; + /// use ethers_core::types::{Address, H256}; + /// use std::{str::FromStr, convert::TryFrom}; + /// + /// # async fn foo() -> eyre::Result<()> { + /// let provider = Provider::::try_from("http://localhost:8545")?; + /// let cast = Cast::new(provider); + /// let addr = Address::from_str("0x00000000006c3852cbEf3e08E8dF289169EdE581")?; + /// let slot = H256::zero(); + /// let storage = cast.storage(addr, slot, None).await?; + /// println!("{}", storage); + /// # Ok(()) + /// # } + /// ``` + pub async fn storage + Send + Sync>( + &self, + from: T, + slot: H256, + block: Option, + ) -> Result { + Ok(format!("{:?}", self.provider.get_storage_at(from, slot, block).await?)) + } } pub struct InterfaceSource { diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4e899de9..61fe1ea7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -56,6 +56,7 @@ dunce = "1.0.2" glob = "0.3.0" globset = "0.4.8" path-slash = "0.2.0" +tempfile = "3.3.0" # misc eyre = "0.6" diff --git a/cli/src/cast.rs b/cli/src/cast.rs index f56d4f4b..7df69519 100644 --- a/cli/src/cast.rs +++ b/cli/src/cast.rs @@ -295,13 +295,7 @@ async fn main() -> eyre::Result<()> { println!("{}", serde_json::to_string(&value)?); } Subcommands::Rpc(cmd) => cmd.run()?.await?, - Subcommands::Storage { address, slot, rpc_url, block } => { - let rpc_url = try_consume_config_rpc_url(rpc_url)?; - - let provider = try_get_http_provider(rpc_url)?; - let value = provider.get_storage_at(address, slot, block).await?; - println!("{value:?}"); - } + Subcommands::Storage(cmd) => cmd.run().await?, // Calls & transactions Subcommands::Call(cmd) => cmd.run().await?, diff --git a/cli/src/cmd/cast/mod.rs b/cli/src/cmd/cast/mod.rs index 7f52a6f9..16592877 100644 --- a/cli/src/cmd/cast/mod.rs +++ b/cli/src/cmd/cast/mod.rs @@ -13,4 +13,5 @@ pub mod interface; pub mod rpc; pub mod run; pub mod send; +pub mod storage; pub mod wallet; diff --git a/cli/src/cmd/cast/storage.rs b/cli/src/cmd/cast/storage.rs new file mode 100644 index 00000000..07de229a --- /dev/null +++ b/cli/src/cmd/cast/storage.rs @@ -0,0 +1,256 @@ +use crate::{ + cmd::forge::build, + opts::cast::{parse_block_id, parse_name_or_address, parse_slot}, + utils::try_consume_config_rpc_url, +}; +use cast::Cast; +use clap::Parser; +use comfy_table::Table; +use ethers::{ + abi::ethabi::ethereum_types::BigEndianHash, etherscan::Client, prelude::*, + solc::artifacts::StorageLayout, +}; +use eyre::{ContextCompat, Result}; +use foundry_common::{ + abi::find_source, + compile::{compile, etherscan_project, suppress_compile}, + try_get_http_provider, RetryProvider, +}; +use foundry_config::Config; +use futures::future::join_all; +use semver::Version; + +/// The minimum Solc version for outputting storage layouts. +/// +/// https://github.com/ethereum/solidity/blob/develop/Changelog.md#065-2020-04-06 +const MIN_SOLC: Version = Version::new(0, 6, 5); + +#[derive(Debug, Clone, Parser)] +pub struct StorageArgs { + // Storage + #[clap(help = "The contract address.", value_parser = parse_name_or_address, value_name = "ADDRESS")] + address: NameOrAddress, + #[clap( + help = "The storage slot number (hex or decimal)", + value_parser = parse_slot, + value_name = "SLOT" + )] + slot: Option, + #[clap(long, env = "ETH_RPC_URL", value_name = "URL")] + rpc_url: Option, + #[clap( + long, + short = 'B', + help = "The block height you want to query at.", + long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", + value_parser = parse_block_id, + value_name = "BLOCK" + )] + block: Option, + + // Etherscan + #[clap(long, short, env = "ETHERSCAN_API_KEY", help = "etherscan API key", value_name = "KEY")] + etherscan_api_key: Option, + #[clap( + long, + visible_alias = "chain-id", + env = "CHAIN", + help = "The chain ID the contract is deployed to.", + default_value = "mainnet", + value_name = "CHAIN" + )] + chain: Chain, + + // Forge + #[clap(flatten)] + build: build::CoreBuildArgs, +} + +impl StorageArgs { + pub async fn run(self) -> Result<()> { + let Self { address, block, build, rpc_url, slot, chain, etherscan_api_key } = self; + + let rpc_url = try_consume_config_rpc_url(rpc_url)?; + let provider = try_get_http_provider(rpc_url)?; + + let address = match address { + NameOrAddress::Name(name) => provider.resolve_name(&name).await?, + NameOrAddress::Address(address) => address, + }; + + // Slot was provided, perform a simple RPC call + if let Some(slot) = slot { + let cast = Cast::new(provider); + println!("{}", cast.storage(address, slot, block).await?); + return Ok(()) + } + + // No slot was provided + // Get deployed bytecode at given address + let address_code = provider.get_code(address, block).await?; + if address_code.is_empty() { + eyre::bail!("Provided address has no deployed code and thus no storage"); + } + + // Check if we're in a forge project + let mut project = build.project()?; + if project.paths.has_input_files() { + // Find in artifacts and pretty print + add_storage_layout_output(&mut project); + let out = compile(&project, false, false)?; + let match_code = |artifact: &ConfigurableContractArtifact| -> Option { + let bytes = + artifact.deployed_bytecode.as_ref()?.bytecode.as_ref()?.object.as_bytes()?; + Some(bytes == &address_code) + }; + let artifact = + out.artifacts().find(|(_, artifact)| match_code(artifact).unwrap_or_default()); + if let Some((_, artifact)) = artifact { + return fetch_and_print_storage(provider, address, artifact, true).await + } + } + + // Not a forge project or artifact not found + // Get code from Etherscan + println!("No matching artifacts found, fetching source code from Etherscan..."); + let api_key = etherscan_api_key.or_else(|| { + let config = Config::load(); + config.get_etherscan_api_key(Some(chain)) + }).wrap_err("No Etherscan API Key is set. Consider using the ETHERSCAN_API_KEY env var, or setting the -e CLI argument or etherscan-api-key in foundry.toml")?; + let client = Client::new(chain, api_key)?; + let source = find_source(client, address).await?; + let metadata = source.items.first().unwrap(); + if metadata.is_vyper() { + eyre::bail!("Contract at provided address is not a valid Solidity contract") + } + + let version = metadata.compiler_version()?; + let auto_detect = version < MIN_SOLC; + + // Create a new temp project + // TODO: Cache instead of using a temp directory: metadata from Etherscan won't change + let root = tempfile::tempdir()?; + let root_path = root.path(); + let mut project = etherscan_project(metadata, root_path)?; + add_storage_layout_output(&mut project); + project.auto_detect = auto_detect; + + // Compile + let mut out = suppress_compile(&project)?; + let artifact = { + let (_, mut artifact) = out + .artifacts() + .find(|(name, _)| name == &metadata.contract_name) + .ok_or_else(|| eyre::eyre!("Could not find artifact"))?; + + if is_storage_layout_empty(&artifact.storage_layout) && auto_detect { + // try recompiling with the minimum version + println!("The requested contract was compiled with {version} while the minimum version for storage layouts is {MIN_SOLC} and as a result the output may be empty."); + let solc = Solc::find_or_install_svm_version(MIN_SOLC.to_string())?; + project.solc = solc; + project.auto_detect = false; + if let Ok(output) = suppress_compile(&project) { + out = output; + let (_, new_artifact) = out + .artifacts() + .find(|(name, _)| name == &metadata.contract_name) + .ok_or_else(|| eyre::eyre!("Could not find artifact"))?; + artifact = new_artifact; + } + } + + artifact + }; + + // Clear temp directory + root.close()?; + + fetch_and_print_storage(provider, address, artifact, true).await + } +} + +async fn fetch_and_print_storage( + provider: RetryProvider, + address: Address, + artifact: &ConfigurableContractArtifact, + pretty: bool, +) -> Result<()> { + if is_storage_layout_empty(&artifact.storage_layout) { + println!("Storage layout is empty."); + Ok(()) + } else { + let mut layout = artifact.storage_layout.as_ref().unwrap().clone(); + fetch_storage_values(provider, address, &mut layout).await?; + print_storage(layout, pretty) + } +} + +/// Overrides the `value` field in [StorageLayout] with the slot's value to avoid creating new data +/// structures. +async fn fetch_storage_values( + provider: RetryProvider, + address: Address, + layout: &mut StorageLayout, +) -> Result<()> { + // TODO: Batch request; handle array values; + let futures: Vec<_> = layout + .storage + .iter() + .map(|slot| { + let slot_h256 = H256::from_uint(&slot.slot.parse::()?); + Ok(provider.get_storage_at(address, slot_h256, None)) + }) + .collect::>()?; + + for (value, slot) in join_all(futures).await.into_iter().zip(layout.storage.iter()) { + let value = value?.into_uint(); + let t = layout.types.get_mut(&slot.storage_type).expect("Bad storage"); + // TODO: Better format values according to their Solidity type + t.value = Some(format!("{value:?}")); + } + + Ok(()) +} + +fn print_storage(layout: StorageLayout, pretty: bool) -> Result<()> { + if !pretty { + println!("{}", serde_json::to_string_pretty(&serde_json::to_value(layout)?)?); + return Ok(()) + } + + let mut table = Table::new(); + table.set_header(vec!["Name", "Type", "Slot", "Offset", "Bytes", "Value", "Contract"]); + + for slot in layout.storage { + let storage_type = layout.types.get(&slot.storage_type); + table.add_row(vec![ + slot.label, + storage_type.as_ref().map_or("?".to_string(), |t| t.label.clone()), + slot.slot, + slot.offset.to_string(), + storage_type.as_ref().map_or("?".to_string(), |t| t.number_of_bytes.clone()), + storage_type + .as_ref() + .map_or("?".to_string(), |t| t.value.clone().unwrap_or_else(|| "0".to_string())), + slot.contract, + ]); + } + + println!("{table}"); + + Ok(()) +} + +fn add_storage_layout_output(project: &mut Project) { + project.artifacts.additional_values.storage_layout = true; + let output_selection = project.artifacts.output_selection(); + project.solc_config.settings.push_all(output_selection); +} + +fn is_storage_layout_empty(storage_layout: &Option) -> bool { + if let Some(ref s) = storage_layout { + s.storage.is_empty() + } else { + true + } +} diff --git a/cli/src/cmd/forge/inspect.rs b/cli/src/cmd/forge/inspect.rs index 54def933..b3310898 100644 --- a/cli/src/cmd/forge/inspect.rs +++ b/cli/src/cmd/forge/inspect.rs @@ -12,7 +12,10 @@ use ethers::{ }, info::ContractInfo, }, - solc::{artifacts::LosslessAbi, utils::canonicalize}, + solc::{ + artifacts::{LosslessAbi, StorageLayout}, + utils::canonicalize, + }, }; use foundry_common::compile; use serde_json::{to_value, Value}; @@ -133,35 +136,7 @@ impl Cmd for InspectArgs { println!("{}", serde_json::to_string_pretty(&to_value(&artifact.gas_estimates)?)?); } ContractArtifactFields::StorageLayout => { - if pretty { - if let Some(storage_layout) = &artifact.storage_layout { - let mut table = Table::new(); - table.load_preset(ASCII_MARKDOWN); - table.set_header(vec![ - "Name", "Type", "Slot", "Offset", "Bytes", "Contract", - ]); - - for slot in &storage_layout.storage { - let storage_type = storage_layout.types.get(&slot.storage_type); - table.add_row(vec![ - slot.label.clone(), - storage_type.as_ref().map_or("?".to_string(), |t| t.label.clone()), - slot.slot.clone(), - slot.offset.to_string(), - storage_type - .as_ref() - .map_or("?".to_string(), |t| t.number_of_bytes.clone()), - slot.contract.clone(), - ]); - } - println!("{table}"); - } - } else { - println!( - "{}", - serde_json::to_string_pretty(&to_value(&artifact.storage_layout)?)? - ); - } + print_storage_layout(&artifact.storage_layout, pretty)?; } ContractArtifactFields::DevDoc => { println!("{}", serde_json::to_string_pretty(&to_value(&artifact.devdoc)?)?); @@ -218,6 +193,42 @@ impl Cmd for InspectArgs { } } +pub fn print_storage_layout( + storage_layout: &Option, + pretty: bool, +) -> eyre::Result<()> { + if storage_layout.is_none() { + eyre::bail!("Could not get storage layout") + } + + let storage_layout = storage_layout.as_ref().unwrap(); + + if !pretty { + println!("{}", serde_json::to_string_pretty(&to_value(storage_layout)?)?); + return Ok(()) + } + + let mut table = Table::new(); + table.load_preset(ASCII_MARKDOWN); + table.set_header(vec!["Name", "Type", "Slot", "Offset", "Bytes", "Contract"]); + + for slot in &storage_layout.storage { + let storage_type = storage_layout.types.get(&slot.storage_type); + table.add_row(vec![ + slot.label.clone(), + storage_type.as_ref().map_or("?".to_string(), |t| t.label.clone()), + slot.slot.clone(), + slot.offset.to_string(), + storage_type.as_ref().map_or("?".to_string(), |t| t.number_of_bytes.clone()), + slot.contract.clone(), + ]); + } + + println!("{table}"); + + Ok(()) +} + /// Contract level output selection #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum ContractArtifactFields { diff --git a/cli/src/opts/cast.rs b/cli/src/opts/cast.rs index d429fb8c..c6b40e1b 100644 --- a/cli/src/opts/cast.rs +++ b/cli/src/opts/cast.rs @@ -3,7 +3,7 @@ use crate::{ cmd::cast::{ call::CallArgs, create2::Create2Args, estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, - wallet::WalletSubcommands, + storage::StorageArgs, wallet::WalletSubcommands, }, utils::parse_u256, }; @@ -625,23 +625,7 @@ Tries to decode the calldata using https://sig.eth.samczsun.com unless --offline visible_alias = "st", about = "Get the raw value of a contract's storage slot." )] - Storage { - #[clap(help = "The contract address.", value_parser = parse_name_or_address, value_name = "ADDRESS")] - address: NameOrAddress, - #[clap(help = "The storage slot number (hex or decimal)", value_parser = parse_slot, value_name = "SLOT")] - slot: H256, - #[clap(short, long, env = "ETH_RPC_URL", value_name = "URL")] - rpc_url: Option, - #[clap( - long, - short = 'B', - help = "The block height you want to query at.", - long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.", - value_parser = parse_block_id, - value_name = "BLOCK" - )] - block: Option, - }, + Storage(StorageArgs), #[clap( name = "proof", visible_alias = "pr", @@ -785,7 +769,7 @@ pub fn parse_block_id(s: &str) -> eyre::Result { }) } -fn parse_slot(s: &str) -> eyre::Result { +pub fn parse_slot(s: &str) -> eyre::Result { Numeric::from_str(s) .map_err(|e| eyre::eyre!("Could not parse slot number: {e}")) .map(|n| H256::from_uint(&n.into())) diff --git a/cli/tests/it/cast.rs b/cli/tests/it/cast.rs index 067b4a36..bbe80b82 100644 --- a/cli/tests/it/cast.rs +++ b/cli/tests/it/cast.rs @@ -240,6 +240,43 @@ casttest!(cast_run_succeeds, |_: TestProject, mut cmd: TestCommand| { assert!(!output.contains("Revert")); }); +// tests that the `cast storage` command works correctly +casttest!(test_live_cast_storage_succeeds, |_: TestProject, mut cmd: TestCommand| { + let eth_rpc_url = next_http_rpc_endpoint(); + + // WETH + // version < min, so empty storage layout + let address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + cmd.cast_fuse().args(["storage", "--rpc-url", eth_rpc_url.as_str(), address]); + let output = cmd.stdout_lossy(); + assert!(output.contains("Storage layout is empty"), "{}", output); + // first slot is the name, always is "Wrapped Ether" + cmd.cast_fuse().args(["storage", "--rpc-url", eth_rpc_url.as_str(), address, "0"]); + let output = cmd.stdout_lossy(); + assert!( + output.contains("0x577261707065642045746865720000000000000000000000000000000000001a"), + "{output}", + ); + + // Polygon bridge proxy + let address = "0xA0c68C638235ee32657e8f720a23ceC1bFc77C77"; + cmd.cast_fuse().args(["storage", "--rpc-url", eth_rpc_url.as_str(), address]); + let output = cmd.stdout_lossy(); + assert!( + output.contains("RootChainManager") && + output.contains("_roles") && + output.contains("mapping(bytes32 => struct AccessControl.RoleData)"), + "{output}", + ); + // first slot is `inited`, always is 1 + cmd.cast_fuse().args(["storage", "--rpc-url", eth_rpc_url.as_str(), address, "0"]); + let output = cmd.stdout_lossy(); + assert!( + output.contains("0x0000000000000000000000000000000000000000000000000000000000000001"), + "{output}", + ); +}); + // tests that `cast --to-base` commands are working correctly. casttest!(cast_to_base, |_: TestProject, mut cmd: TestCommand| { let values = [ diff --git a/cli/tests/it/utils.rs b/cli/tests/it/utils.rs index f403522e..36f11689 100644 --- a/cli/tests/it/utils.rs +++ b/cli/tests/it/utils.rs @@ -24,12 +24,12 @@ pub fn etherscan_key(chain: Chain) -> Option { pub fn network_rpc_key(chain: &str) -> Option { let key = format!("{}_RPC_URL", chain.to_uppercase().replace('-', "_")); - std::env::var(&key).ok() + std::env::var(key).ok() } pub fn network_private_key(chain: &str) -> Option { let key = format!("{}_PRIVATE_KEY", chain.to_uppercase().replace('-', "_")); - std::env::var(&key).or_else(|_| std::env::var("TEST_PRIVATE_KEY")).ok() + std::env::var(key).or_else(|_| std::env::var("TEST_PRIVATE_KEY")).ok() } /// Represents external input required for executing verification requests