Skip to content

feat(cast): Add state overrides flags to cast call #10255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

195 changes: 183 additions & 12 deletions crates/cast/src/cmd/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use crate::{
tx::{CastTxBuilder, SenderKind},
Cast,
};
use alloy_primitives::{TxKind, U256};
use alloy_primitives::{Address, Bytes, TxKind, U256, map::FbBuildHasher};
use alloy_rpc_types::{BlockId, BlockNumberOrTag};
use alloy_rpc_types::state::StateOverride;
use clap::Parser;
use eyre::Result;
use foundry_cli::{
Expand All @@ -27,8 +28,28 @@ use foundry_evm::{
traces::{InternalTraceMode, TraceMode},
};
use std::str::FromStr;
use std::collections::HashMap;

/// CLI arguments for `cast call`.
///
/// ## State Override Flags
///
/// The following flags can be used to override the state for the call:
///
/// * `--override-balance <address>:<balance>` - Override the balance of an account
/// * `--override-nonce <address>:<nonce>` - Override the nonce of an account
/// * `--override-code <address>:<code>` - Override the code of an account
/// * `--override-state <address>:<slot>:<value>` - 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
/// ```
#[derive(Debug, Parser)]
pub struct CallArgs {
/// The destination of the transaction.
Expand Down Expand Up @@ -92,6 +113,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: Vec<String>,

/// Override the nonce of an account.
/// Format: address:nonce
#[arg(long = "override-nonce", value_name = "ADDRESS:NONCE")]
pub nonce_overrides: Vec<String>,

/// Override the code of an account.
/// Format: address:code
#[arg(long = "override-code", value_name = "ADDRESS:CODE")]
pub code_overrides: Vec<String>,

/// Override the state of an account.
/// Format: address:slot:value
#[arg(long = "override-state", value_name = "ADDRESS:SLOT:VALUE")]
pub state_overrides: Vec<String>,

/// 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: Vec<String>,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -123,6 +169,7 @@ impl CallArgs {
let figment = Into::<Figment>::into(&self.eth).merge(&self);
let evm_opts = figment.extract::<EvmOpts>()?;
let mut config = Config::from_provider(figment)?.sanitized();
let state_override = &self.get_state_overrides()?;

let Self {
to,
Expand All @@ -142,6 +189,7 @@ impl CallArgs {
..
} = self;


if let Some(data) = data {
sig = Some(data);
}
Expand Down Expand Up @@ -236,10 +284,100 @@ 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_override.clone()).await?)?;

Ok(())
}
/// Parse state overrides from command line arguments
pub fn get_state_overrides(&self) -> eyre::Result<Option<StateOverride>> {
let mut state_override = StateOverride::default();

// Store state_diff_overrides in a local variable to avoid partial move
let balance_overrides = self.balance_overrides.clone();
// Parse balance overrides
for override_str in balance_overrides {
let (addr, balance) = Self::parse_address_value(&override_str)?;
state_override.entry(addr).or_default().balance = Some(balance);
}

// Store state_diff_overrides in a local variable to avoid partial move
let nonce_overrides = self.nonce_overrides.clone();
// Parse nonce overrides
for override_str in nonce_overrides {
let (addr, nonce) = Self::parse_address_value_for_nonce(&override_str)?;
state_override.entry(addr).or_default().nonce = Some(nonce);
}

// Store state_diff_overrides in a local variable to avoid partial move
let code_overrides = self.code_overrides.clone();
// Parse code overrides
for override_str in code_overrides {
let (addr, code_str) = override_str.split_once(':').ok_or_else(|| {
eyre::eyre!("Invalid code override format. Expected <address>:<code>")
})?;
let addr = addr.parse()?;
let code = Bytes::from_str(code_str)?;
state_override.entry(addr).or_default().code = Some(code);
}

// Store state_diff_overrides in a local variable to avoid partial move
let state_overrides = self.state_overrides.clone();
// Parse state overrides
for override_str in state_overrides {
let (addr, slot, value) = Self::parse_address_slot_value(&override_str)?;
let state_map = state_override.entry(addr).or_default().state.get_or_insert_with(|| HashMap::with_hasher(FbBuildHasher::<32>::default()));
state_map.insert(slot.into(), value.into());
}

// Store state_diff_overrides in a local variable to avoid partial move
let state_diff_overrides = self.state_diff_overrides.clone();
// Parse state diff overrides
for override_str in state_diff_overrides {
let (addr, slot, value) = Self::parse_address_slot_value(&override_str)?;
let state_diff_map = state_override.entry(addr).or_default().state_diff.get_or_insert_with(|| HashMap::with_hasher(FbBuildHasher::<32>::default()));
state_diff_map.insert(slot.into(), value.into());
}

Ok(if state_override.is_empty() {
None
} else {
Some(state_override)
})
}

/// Parse an override string in the format address:value
pub fn parse_address_value(s: &str) -> eyre::Result<(Address, U256)> {
let (addr, value) = s.split_once(':').ok_or_else(|| {
eyre::eyre!("Invalid override format. Expected <address>:<value>")
})?;
Ok((addr.parse()?, value.parse()?))
}

/// Parse an override string in the format address:value
pub fn parse_address_value_for_nonce( s: &str) -> eyre::Result<(Address, u64)> {
let (addr, value) = s.split_once(':').ok_or_else(|| {
eyre::eyre!("Invalid override format. Expected <address>:<value>")
})?;
Ok((addr.parse()?, value.parse()?))
}

/// Parse an override string in the format address:slot:value
pub fn parse_address_slot_value(s: &str) -> eyre::Result<(Address, U256, U256)> {
let mut parts = s.split(':');
let addr = parts.next().ok_or_else(|| {
eyre::eyre!("Invalid override format. Expected <address>:<slot>:<value>")
})?.parse()?;
let slot = parts.next().ok_or_else(|| {
eyre::eyre!("Invalid override format. Expected <address>:<slot>:<value>")
})?.parse()?;
let value = parts.next().ok_or_else(|| {
eyre::eyre!("Invalid override format. Expected <address>:<slot>:<value>")
})?.parse()?;
if parts.next().is_some() {
return Err(eyre::eyre!("Invalid override format. Expected <address>:<slot>:<value>"));
}
Ok((addr, slot, value))
}
}

impl figment::Provider for CallArgs {
Expand All @@ -265,7 +403,7 @@ impl figment::Provider for CallArgs {
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{hex, Address};
use alloy_primitives::{hex};

#[test]
fn can_parse_call_data() {
Expand All @@ -279,17 +417,50 @@ 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, vec!["0x123:0x1234"]);
assert_eq!(args.nonce_overrides, vec!["0x123:1"]);
assert_eq!(args.code_overrides, vec!["0x123:0x1234"]);
assert_eq!(args.state_overrides, vec!["0x123:0x1:0x1234"]);
}

#[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, vec!["0x123:0x1234", "0x456:0x5678"]);
assert_eq!(args.nonce_overrides, vec!["0x123:1", "0x456:2"]);
assert_eq!(args.code_overrides, vec!["0x123:0x1234", "0x456:0x5678"]);
assert_eq!(args.state_overrides, vec!["0x123:0x1:0x1234", "0x456:0x2:0x5678"]);
}
}
24 changes: 18 additions & 6 deletions crates/cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use alloy_provider::{
PendingTransactionBuilder, Provider,
};
use alloy_rlp::Decodable;
use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter, TransactionRequest};
use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter, TransactionRequest, state::StateOverride};
use alloy_serde::WithOtherFields;
use alloy_sol_types::sol;
use base::{Base, NumberWithBase, ToBase};
Expand Down Expand Up @@ -106,26 +106,34 @@ impl<P: Provider<AnyNetwork>> Cast<P> {
///
/// ```
/// 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_sol_types::{sol, SolCall};
///
/// sol!(
/// function greeting(uint256 i) public returns (string);
/// );
///
/// # 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 = Some(state_override);
///
/// 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(())
/// # }
Expand All @@ -135,8 +143,12 @@ impl<P: Provider<AnyNetwork>> Cast<P> {
req: &WithOtherFields<TransactionRequest>,
func: Option<&Function>,
block: Option<BlockId>,
state_override: Option<StateOverride>,
) -> Result<String> {
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.unwrap_or_default())
.await?;

let mut decoded = vec![];

Expand Down
31 changes: 31 additions & 0 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,37 @@ 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 (_api, handle) = anvil::spawn(
NodeConfig::test()
.with_auto_impersonate(true)
.with_eth_rpc_url(Some("https://sepolia.base.org")),
)
.await;

let http_endpoint = handle.http_endpoint();

cmd.cast_fuse()
.args([
"call",
"5FbDB2315678afecb367f032d93F642f64180aa3",
"--rpc-url",
&http_endpoint,
"--override-balance",
"5FbDB2315678afecb367f032d93F642f64180aa3:1234",
"--override-nonce",
"5FbDB2315678afecb367f032d93F642f64180aa3:5",
// "--override-code",
// "5FbDB2315678afecb367f032d93F642f64180aa3:0x1234",
"--override-state",
"5FbDB2315678afecb367f032d93F642f64180aa3:0x1:1234",
// "--override-state-diff",
// "5FbDB2315678afecb367f032d93F642f64180aa3:0x2:1234",
])
.assert_success();
});

// https://github.com/foundry-rs/foundry/issues/9541
forgetest_async!(cast_run_impersonated_tx, |_prj, cmd| {
let (_api, handle) = anvil::spawn(
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ alloy-primitives.workspace = true
alloy-provider.workspace = true
alloy-rlp.workspace = true
alloy-chains.workspace = true
alloy-rpc-types.workspace = true

clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }
color-eyre.workspace = true
Expand Down
Loading