diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 053bad5d2c664..65ee4c068c17b 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -3,8 +3,11 @@ use crate::{ tx::{CastTxBuilder, SenderKind}, Cast, }; -use alloy_primitives::{TxKind, U256}; -use alloy_rpc_types::{BlockId, BlockNumberOrTag}; +use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_rpc_types::{ + state::{StateOverride, StateOverridesBuilder}, + BlockId, BlockNumberOrTag, +}; use clap::Parser; use eyre::Result; use foundry_cli::{ @@ -26,9 +29,35 @@ use foundry_evm::{ opts::EvmOpts, traces::{InternalTraceMode, TraceMode}, }; -use std::str::FromStr; +use regex::Regex; +use std::{str::FromStr, sync::LazyLock}; + +// matches override pattern
:: +// e.g. 0x123:0x1:0x1234 +static OVERRIDE_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"^([^:]+):([^:]+):([^:]+)$").unwrap()); /// CLI arguments for `cast call`. +/// +/// ## State Override Flags +/// +/// The following flags can be used to override the state for the call: +/// +/// * `--override-balance
:` - Override the balance of an account +/// * `--override-nonce
:` - Override the nonce of an account +/// * `--override-code
:` - Override the code of an account +/// * `--override-state
::` - Override a storage slot of an account +/// +/// Multiple overrides can be specified for the same account. For example: +/// +/// ```bash +/// cast call 0x... "transfer(address,uint256)" 0x... 100 \ +/// --override-balance 0x123:0x1234 \ +/// --override-nonce 0x123:1 \ +/// --override-code 0x123:0x1234 \ +/// --override-state 0x123:0x1:0x1234 +/// --override-state-diff 0x123:0x1:0x1234 +/// ``` #[derive(Debug, Parser)] pub struct CallArgs { /// The destination of the transaction. @@ -92,6 +121,31 @@ pub struct CallArgs { /// Use current project artifacts for trace decoding. #[arg(long, visible_alias = "la")] pub with_local_artifacts: bool, + + /// Override the balance of an account. + /// Format: address:balance + #[arg(long = "override-balance", value_name = "ADDRESS:BALANCE")] + pub balance_overrides: Option>, + + /// Override the nonce of an account. + /// Format: address:nonce + #[arg(long = "override-nonce", value_name = "ADDRESS:NONCE")] + pub nonce_overrides: Option>, + + /// Override the code of an account. + /// Format: address:code + #[arg(long = "override-code", value_name = "ADDRESS:CODE")] + pub code_overrides: Option>, + + /// Override the state of an account. + /// Format: address:slot:value + #[arg(long = "override-state", value_name = "ADDRESS:SLOT:VALUE")] + pub state_overrides: Option>, + + /// Override the state diff of an account. + /// Format: address:slot:value + #[arg(long = "override-state-diff", value_name = "ADDRESS:SLOT:VALUE")] + pub state_diff_overrides: Option>, } #[derive(Debug, Parser)] @@ -123,6 +177,7 @@ impl CallArgs { let figment = Into::::into(&self.eth).merge(&self); let evm_opts = figment.extract::()?; let mut config = Config::from_provider(figment)?.sanitized(); + let state_overrides = self.get_state_overrides()?; let Self { to, @@ -236,10 +291,54 @@ impl CallArgs { return Ok(()); } - sh_println!("{}", Cast::new(provider).call(&tx, func.as_ref(), block).await?)?; + sh_println!( + "{}", + Cast::new(provider).call(&tx, func.as_ref(), block, state_overrides).await? + )?; Ok(()) } + /// Parse state overrides from command line arguments + pub fn get_state_overrides(&self) -> eyre::Result { + let mut state_overrides_builder = StateOverridesBuilder::default(); + + // Parse balance overrides + for override_str in self.balance_overrides.iter().flatten() { + let (addr, balance) = address_value_override(override_str)?; + state_overrides_builder = + state_overrides_builder.with_balance(addr.parse()?, balance.parse()?); + } + + // Parse nonce overrides + for override_str in self.nonce_overrides.iter().flatten() { + let (addr, nonce) = address_value_override(override_str)?; + state_overrides_builder = + state_overrides_builder.with_nonce(addr.parse()?, nonce.parse()?); + } + + // Parse code overrides + for override_str in self.code_overrides.iter().flatten() { + let (addr, code_str) = address_value_override(override_str)?; + state_overrides_builder = + state_overrides_builder.with_code(addr.parse()?, Bytes::from_str(code_str)?); + } + + // Parse state overrides + for override_str in self.state_overrides.iter().flatten() { + let (addr, slot, value) = address_slot_value_override(override_str)?; + state_overrides_builder = + state_overrides_builder.with_state(addr, [(slot.into(), value.into())]); + } + + // Parse state diff overrides + for override_str in self.state_diff_overrides.iter().flatten() { + let (addr, slot, value) = address_slot_value_override(override_str)?; + state_overrides_builder = + state_overrides_builder.with_state_diff(addr, [(slot.into(), value.into())]); + } + + Ok(state_overrides_builder.build()) + } } impl figment::Provider for CallArgs { @@ -262,10 +361,30 @@ impl figment::Provider for CallArgs { } } +/// Parse an override string in the format address:value. +fn address_value_override(address_override: &str) -> Result<(&str, &str)> { + address_override.split_once(':').ok_or_else(|| { + eyre::eyre!("Invalid override {address_override}. Expected
:") + }) +} + +/// Parse an override string in the format address:slot:value. +fn address_slot_value_override(address_override: &str) -> Result<(Address, U256, U256)> { + let captures = OVERRIDE_PATTERN.captures(address_override).ok_or_else(|| { + eyre::eyre!("Invalid override {address_override}. Expected
::") + })?; + + Ok(( + captures[1].parse()?, // Address + captures[2].parse()?, // Slot (U256) + captures[3].parse()?, // Value (U256) + )) +} + #[cfg(test)] mod tests { use super::*; - use alloy_primitives::{hex, Address}; + use alloy_primitives::hex; #[test] fn can_parse_call_data() { @@ -279,17 +398,59 @@ mod tests { } #[test] - fn call_sig_and_data_exclusive() { - let data = hex::encode("hello"); - let to = Address::ZERO; - let args = CallArgs::try_parse_from([ + fn can_parse_state_overrides() { + let args = CallArgs::parse_from([ + "foundry-cli", + "--override-balance", + "0x123:0x1234", + "--override-nonce", + "0x123:1", + "--override-code", + "0x123:0x1234", + "--override-state", + "0x123:0x1:0x1234", + ]); + + assert_eq!(args.balance_overrides, Some(vec!["0x123:0x1234".to_string()])); + assert_eq!(args.nonce_overrides, Some(vec!["0x123:1".to_string()])); + assert_eq!(args.code_overrides, Some(vec!["0x123:0x1234".to_string()])); + assert_eq!(args.state_overrides, Some(vec!["0x123:0x1:0x1234".to_string()])); + } + + #[test] + fn can_parse_multiple_state_overrides() { + let args = CallArgs::parse_from([ "foundry-cli", - to.to_string().as_str(), - "signature", - "--data", - format!("0x{data}").as_str(), + "--override-balance", + "0x123:0x1234", + "--override-balance", + "0x456:0x5678", + "--override-nonce", + "0x123:1", + "--override-nonce", + "0x456:2", + "--override-code", + "0x123:0x1234", + "--override-code", + "0x456:0x5678", + "--override-state", + "0x123:0x1:0x1234", + "--override-state", + "0x456:0x2:0x5678", ]); - assert!(args.is_err()); + assert_eq!( + args.balance_overrides, + Some(vec!["0x123:0x1234".to_string(), "0x456:0x5678".to_string()]) + ); + assert_eq!(args.nonce_overrides, Some(vec!["0x123:1".to_string(), "0x456:2".to_string()])); + assert_eq!( + args.code_overrides, + Some(vec!["0x123:0x1234".to_string(), "0x456:0x5678".to_string()]) + ); + assert_eq!( + args.state_overrides, + Some(vec!["0x123:0x1:0x1234".to_string(), "0x456:0x2:0x5678".to_string()]) + ); } } diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index a83e67ef2183f..66a0abc75f78f 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -16,7 +16,9 @@ use alloy_provider::{ PendingTransactionBuilder, Provider, }; use alloy_rlp::Decodable; -use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter, TransactionRequest}; +use alloy_rpc_types::{ + state::StateOverride, BlockId, BlockNumberOrTag, Filter, TransactionRequest, +}; use alloy_serde::WithOtherFields; use alloy_sol_types::sol; use base::{Base, NumberWithBase, ToBase}; @@ -107,11 +109,12 @@ impl> Cast

{ /// /// ``` /// use alloy_primitives::{Address, U256, Bytes}; - /// use alloy_rpc_types::{TransactionRequest}; + /// use alloy_rpc_types::{TransactionRequest, state::{StateOverride, AccountOverride}}; /// use alloy_serde::WithOtherFields; /// use cast::Cast; /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork}; - /// use std::str::FromStr; + /// use std::{str::FromStr, collections::HashMap}; + /// use alloy_rpc_types::state::StateOverridesBuilder; /// use alloy_sol_types::{sol, SolCall}; /// /// sol!( @@ -119,14 +122,22 @@ impl> Cast

{ /// ); /// /// # async fn foo() -> eyre::Result<()> { - /// let alloy_provider = ProviderBuilder::<_,_, AnyNetwork>::default().on_builtin("http://localhost:8545").await?;; + /// let alloy_provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;; /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?; /// let greeting = greetingCall { i: U256::from(5) }.abi_encode(); /// let bytes = Bytes::from_iter(greeting.iter()); /// let tx = TransactionRequest::default().to(to).input(bytes.into()); /// let tx = WithOtherFields::new(tx); + /// + /// // Create state overrides + /// let mut state_override = StateOverride::default(); + /// let mut account_override = AccountOverride::default(); + /// account_override.balance = Some(U256::from(1000)); + /// state_override.insert(to, account_override); + /// let state_override_object = StateOverridesBuilder::default().build(); + /// /// let cast = Cast::new(alloy_provider); - /// let data = cast.call(&tx, None, None).await?; + /// let data = cast.call(&tx, None, None, state_override_object).await?; /// println!("{}", data); /// # Ok(()) /// # } @@ -136,8 +147,14 @@ impl> Cast

{ req: &WithOtherFields, func: Option<&Function>, block: Option, + state_override: StateOverride, ) -> Result { - let res = self.provider.call(req.clone()).block(block.unwrap_or_default()).await?; + let res = self + .provider + .call(req.clone()) + .block(block.unwrap_or_default()) + .overrides(state_override) + .await?; let mut decoded = vec![]; diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index c37ea2bbed221..be2e9a7ed7101 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -2257,6 +2257,132 @@ forgetest_async!(cast_call_custom_chain_id, |_prj, cmd| { .assert_success(); }); +// https://github.com/foundry-rs/foundry/issues/10189 +forgetest_async!(cast_call_custom_override, |prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test()).await; + + foundry_test_utils::util::initialize(prj.root()); + prj.add_source( + "Counter", + r#" +contract Counter { + uint256 public number; + + function getBalance(address target) public returns (uint256) { + return target.balance; + } +} + "#, + ) + .unwrap(); + + // Deploy counter contract. + cmd.args([ + "script", + "--private-key", + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--rpc-url", + &handle.http_endpoint(), + "--broadcast", + "CounterScript", + ]) + .assert_success(); + + // Override state, `number()` should return overridden value. + cmd.cast_fuse() + .args([ + "call", + "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "--rpc-url", + &handle.http_endpoint(), + "--override-state", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x0:0x1234", + "number()(uint256)", + ]) + .assert_success() + .stdout_eq(str![[r#" +4660 + +"#]]); + + // Override balance, `getBalance()` should return overridden value. + cmd.cast_fuse() + .args([ + "call", + "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "--rpc-url", + &handle.http_endpoint(), + "--override-balance", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x1111", + "getBalance(address)(uint256)", + "0x5FbDB2315678afecb367f032d93F642f64180aa3", + ]) + .assert_success() + .stdout_eq(str![[r#" +4369 + +"#]]); + + // Override code with + // contract Counter { + // uint256 public number1; + // } + // Calling `number()` should fail. + cmd.cast_fuse() + .args([ + "call", + "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "--rpc-url", + &handle.http_endpoint(), + "--override-code", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063c223a39e14602a575b5f5ffd5b60306044565b604051603b9190605f565b60405180910390f35b5f5481565b5f819050919050565b6059816049565b82525050565b5f60208201905060705f8301846052565b9291505056fea26469706673582212202a0acfb9083efed3e0e9f27177b090731d4392cf196d58e27e05088f59008d0964736f6c634300081d0033", + "number()(uint256)", + ]) + .assert_failure() + .stderr_eq(str![[r#" +Error: server returned an error response: error code 3: execution reverted, data: "0x" + +"#]]); + + // Calling `number1()` with overridden state should return new value. + cmd.cast_fuse() + .args([ + "call", + "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "--rpc-url", + &handle.http_endpoint(), + "--override-code", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063c223a39e14602a575b5f5ffd5b60306044565b604051603b9190605f565b60405180910390f35b5f5481565b5f819050919050565b6059816049565b82525050565b5f60208201905060705f8301846052565b9291505056fea26469706673582212202a0acfb9083efed3e0e9f27177b090731d4392cf196d58e27e05088f59008d0964736f6c634300081d0033", + "--override-state", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x0:0x2222", + "number1()(uint256)", + ]) + .assert_success() + .stdout_eq(str![[r#" +8738 + +"#]]); + + // Calling `number1()` with overridden state should return new value. + cmd.cast_fuse() + .args([ + "call", + "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "--rpc-url", + &handle.http_endpoint(), + "--override-code", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063c223a39e14602a575b5f5ffd5b60306044565b604051603b9190605f565b60405180910390f35b5f5481565b5f819050919050565b6059816049565b82525050565b5f60208201905060705f8301846052565b9291505056fea26469706673582212202a0acfb9083efed3e0e9f27177b090731d4392cf196d58e27e05088f59008d0964736f6c634300081d0033", + "--override-state-diff", + "0x5FbDB2315678afecb367f032d93F642f64180aa3:0x0:0x2222", + "number1()(uint256)", + ]) + .assert_success() + .stdout_eq(str![[r#" +8738 + +"#]]); +}); + // https://github.com/foundry-rs/foundry/issues/9541 forgetest_async!(cast_run_impersonated_tx, |_prj, cmd| { let (_api, handle) = anvil::spawn(