Skip to main content

cast/cmd/
call.rs

1use super::{
2    call_overrides::CallOverrideOpts,
3    run::{fetch_contracts_bytecode_from_trace, fetch_contracts_bytecode_via_rpc},
4};
5use crate::{
6    Cast,
7    debug::handle_traces,
8    rpc_trace::{call_frame_to_arena, is_method_not_found_error, is_missing_state_error},
9    traces::TraceKind,
10    tx::{CastTxBuilder, SenderKind},
11};
12use alloy_ens::NameOrAddress;
13use alloy_network::{Network, NetworkTransactionBuilder, TransactionBuilder};
14use alloy_primitives::{Bytes, TxKind, U256, hex, map::AddressHashMap};
15use alloy_provider::{Provider, ext::DebugApi};
16use alloy_rpc_types::{
17    BlockId, BlockNumberOrTag, BlockOverrides,
18    state::StateOverride,
19    trace::geth::{
20        CallConfig, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions,
21        GethDebugTracingOptions, GethTrace,
22    },
23};
24use clap::Parser;
25use eyre::Result;
26use foundry_cli::{
27    opts::{ChainValueParser, RpcOpts, TracingArgs, TransactionOpts},
28    utils::{LoadConfig, TraceResult, load_config_from_provider, parse_ether_value},
29};
30use foundry_common::{
31    FoundryTransactionBuilder,
32    abi::{encode_function_args, get_func},
33    provider::{ProviderBuilder, curl_transport::generate_curl_command},
34    sh_println, shell,
35};
36use foundry_compilers::artifacts::EvmVersion;
37use foundry_config::{
38    Chain, Config, TracingConfig,
39    figment::{
40        self, Metadata, Profile,
41        value::{Dict, Map},
42    },
43};
44#[cfg(feature = "optimism")]
45use foundry_evm::core::evm::OpEvmNetwork;
46use foundry_evm::{
47    core::{
48        FoundryBlock, FoundryTransaction,
49        evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork},
50    },
51    executors::TracingExecutor,
52    opts::EvmOpts,
53    traces::{InternalTraceMode, SparsedTraceArena, TraceRequirements},
54};
55use foundry_wallets::WalletOpts;
56use std::str::FromStr;
57
58/// CLI arguments for `cast call`.
59///
60/// ## State Override Flags
61///
62/// The following flags can be used to override the state for the call:
63///
64/// * `--override-balance <address>:<balance>` - Override the balance of an account
65/// * `--override-nonce <address>:<nonce>` - Override the nonce of an account
66/// * `--override-code <address>:<code>` - Override the code of an account
67/// * `--override-state <address>:<slot>:<value>` - Override a storage slot of an account
68///
69/// Multiple overrides can be specified for the same account. For example:
70///
71/// ```bash
72/// cast call 0x... "transfer(address,uint256)" 0x... 100 \
73///   --override-balance 0x123:0x1234 \
74///   --override-nonce 0x123:1 \
75///   --override-code 0x123:0x1234 \
76///   --override-state 0x123:0x1:0x1234
77///   --override-state-diff 0x123:0x1:0x1234
78/// ```
79#[derive(Debug, Parser)]
80pub struct CallArgs {
81    /// The destination of the transaction.
82    #[arg(value_parser = NameOrAddress::from_str)]
83    to: Option<NameOrAddress>,
84
85    /// The signature of the function to call.
86    sig: Option<String>,
87
88    /// The arguments of the function to call.
89    #[arg(allow_negative_numbers = true)]
90    args: Vec<String>,
91
92    /// Raw hex-encoded data for the transaction. Used instead of \[SIG\] and \[ARGS\].
93    #[arg(
94        long,
95        conflicts_with_all = &["sig", "args"]
96    )]
97    data: Option<String>,
98
99    /// Forks the remote rpc, executes the transaction locally and prints a trace
100    #[arg(long, default_value_t = false)]
101    trace: bool,
102
103    /// Fetch the call trace from the node via `debug_traceCall` (callTracer) and render it,
104    /// instead of re-executing the call locally like `--trace`.
105    ///
106    /// This is a call-tree view: nested calls, value, gas, emitted logs and revert data. It does
107    /// not provide the opcode / struct-log level detail of a local `--trace` / `--debug` run.
108    ///
109    /// The local-execution-only trace flags (`--debug`, `--decode-internal`, `--evm-version`) do
110    /// not apply, since the trace comes from the node rather than a local run.
111    #[arg(
112        long = "debug-trace-call",
113        default_value_t = false,
114        conflicts_with_all = ["trace", "debug", "decode_internal", "evm_version"]
115    )]
116    debug_trace_call: bool,
117
118    /// Opens an interactive debugger.
119    /// Can only be used with `--trace`.
120    #[arg(long, requires = "trace")]
121    debug: bool,
122
123    #[command(flatten)]
124    tracing: TracingArgs,
125
126    /// The EVM Version to use.
127    /// Can only be used with `--trace`.
128    #[arg(long, requires = "trace")]
129    evm_version: Option<EvmVersion>,
130
131    /// The block height to query at.
132    ///
133    /// Can also be the tags earliest, finalized, safe, latest, or pending.
134    #[arg(long, short)]
135    block: Option<BlockId>,
136
137    #[command(subcommand)]
138    command: Option<CallSubcommands>,
139
140    #[command(flatten)]
141    tx: TransactionOpts,
142
143    #[command(flatten)]
144    rpc: RpcOpts,
145
146    #[command(flatten)]
147    wallet: WalletOpts,
148
149    #[arg(
150        short,
151        long,
152        alias = "chain-id",
153        env = "CHAIN",
154        value_parser = ChainValueParser::default(),
155    )]
156    pub chain: Option<Chain>,
157
158    /// Use current project artifacts for trace decoding.
159    #[arg(long, visible_alias = "la")]
160    pub with_local_artifacts: bool,
161
162    #[command(flatten)]
163    pub overrides: CallOverrideOpts,
164}
165
166#[derive(Debug, Parser)]
167pub enum CallSubcommands {
168    /// ignores the address field and simulates creating a contract
169    #[command(name = "--create")]
170    Create {
171        /// Bytecode of contract.
172        code: String,
173
174        /// The signature of the constructor.
175        sig: Option<String>,
176
177        /// The arguments of the constructor.
178        #[arg(allow_negative_numbers = true)]
179        args: Vec<String>,
180
181        /// Ether to send in the transaction.
182        ///
183        /// Either specified in wei, or as a string with a unit type.
184        ///
185        /// Examples: 1ether, 10gwei, 0.01ether
186        #[arg(long, value_parser = parse_ether_value)]
187        value: Option<U256>,
188    },
189}
190
191impl CallArgs {
192    fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
193        if self.debug_trace_call {
194            self.tracing.resolve_call_tracer(config, verbosity)
195        } else {
196            self.tracing.resolve(config, verbosity)
197        }
198    }
199
200    pub async fn run(mut self) -> Result<()> {
201        self.validate_trace_args()?;
202
203        // Handle --curl mode early, before any provider interaction
204        if self.rpc.curl {
205            return self.run_curl().await;
206        }
207
208        if self.tx.tempo.is_tempo() {
209            return self.run_with_network::<TempoEvmNetwork>().await;
210        }
211
212        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
213        let mut evm_opts = figment.extract::<EvmOpts>()?;
214        if let Some(chain) = self.chain {
215            evm_opts.networks = evm_opts.networks.with_chain_id(chain.id());
216        }
217        evm_opts.infer_network_from_fork().await;
218        if self.chain.is_none() {
219            self.chain = evm_opts.env.chain_id.map(Chain::from_id);
220        }
221
222        if evm_opts.networks.is_tempo() {
223            return self.run_with_network::<TempoEvmNetwork>().await;
224        }
225
226        #[cfg(feature = "optimism")]
227        if evm_opts.networks.is_optimism() {
228            return self.run_with_network::<OpEvmNetwork>().await;
229        }
230
231        self.run_with_network::<EthEvmNetwork>().await
232    }
233
234    fn validate_trace_args(&self) -> Result<()> {
235        if !self.trace
236            && !self.debug_trace_call
237            && (self.tracing.disable_labels
238                || self.tracing.compact_labels
239                || !self.tracing.labels.is_empty()
240                || self.tracing.trace_depth.is_some())
241        {
242            eyre::bail!("trace rendering options require `--trace` or `--debug-trace-call`");
243        }
244
245        if self.tracing.decode_internal && !self.trace {
246            eyre::bail!("`--decode-internal` requires `--trace`");
247        }
248
249        Ok(())
250    }
251
252    pub async fn run_with_network<FEN: FoundryEvmNetwork>(self) -> Result<()>
253    where
254        <FEN::Network as Network>::TransactionRequest: FoundryTransactionBuilder<FEN::Network>,
255    {
256        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
257        let evm_opts = figment.extract::<EvmOpts>()?;
258        let mut config = load_config_from_provider(figment)?;
259        let state_overrides = self.get_state_overrides()?;
260        let block_overrides = self.get_block_overrides()?;
261        let tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
262
263        let Self {
264            to,
265            mut sig,
266            mut args,
267            mut tx,
268            command,
269            block,
270            trace,
271            evm_version,
272            debug,
273            data,
274            with_local_artifacts,
275            wallet,
276            ..
277        } = self;
278
279        if let Some(data) = data {
280            sig = Some(data);
281        }
282
283        let provider = ProviderBuilder::<FEN::Network>::from_config(&config)?.build()?;
284        let sender = SenderKind::from_wallet_opts(wallet).await?;
285        let from = sender.address();
286
287        let code = if let Some(CallSubcommands::Create {
288            code,
289            sig: create_sig,
290            args: create_args,
291            value,
292        }) = command
293        {
294            sig = create_sig;
295            args = create_args;
296            if let Some(value) = value {
297                tx.value = Some(value);
298            }
299            Some(code)
300        } else {
301            None
302        };
303
304        let (tx, func) = CastTxBuilder::new(&provider, tx, &config)
305            .await?
306            .with_to(to)
307            .await?
308            .with_code_sig_and_args(code, sig, args)
309            .await?
310            .raw()
311            .build(sender)
312            .await?;
313
314        if self.debug_trace_call {
315            let block = self.block.unwrap_or(BlockId::latest());
316            let mut call_options = GethDebugTracingCallOptions::default().with_tracing_options(
317                GethDebugTracingOptions::default()
318                    .with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
319                    .with_call_config(CallConfig::default().with_log()),
320            );
321            // A contract that only exists through a `--override-code` entry has no on-chain
322            // code to fetch for local-artifact matching, so remember the override code before
323            // handing the overrides to `debug_traceCall`.
324            let mut override_bytecode = AddressHashMap::<Bytes>::default();
325            if with_local_artifacts && let Some(overrides) = &state_overrides {
326                for (address, account) in overrides {
327                    if let Some(code) = &account.code {
328                        override_bytecode.insert(*address, code.clone());
329                    }
330                }
331            }
332
333            // Honour the same state / block overrides as the local `--trace` path.
334            if let Some(state_overrides) = state_overrides {
335                call_options = call_options.with_state_overrides(state_overrides);
336            }
337            if let Some(block_overrides) = block_overrides {
338                call_options = call_options.with_block_overrides(block_overrides);
339            }
340
341            let geth_trace = provider
342                .debug_trace_call(tx, block, call_options)
343                .await
344                .map_err(|err| -> eyre::Report {
345                    // Two RPC rejections deserve an actionable hint instead of the raw transport
346                    // error, and they need different fixes: a disabled `debug` namespace, and
347                    // missing historical state, hit whenever `--block` targets a block a full
348                    // node has pruned.
349                    if is_method_not_found_error(&err) {
350                        eyre::eyre!(
351                            "the RPC endpoint does not support `debug_traceCall` (method not found); use a node with the `debug` namespace enabled (e.g. a local anvil/reth or an archive endpoint), or drop `--debug-trace-call` to run the call locally with `--trace`"
352                        )
353                    } else if is_missing_state_error(&err) {
354                        eyre::eyre!(
355                            "the RPC endpoint does not have the historical state for the requested block; use an archive endpoint, or target a more recent block with `--block`"
356                        )
357                    } else {
358                        err.into()
359                    }
360                })?;
361            let GethTrace::CallTracer(frame) = geth_trace else {
362                eyre::bail!(
363                    "`debug_traceCall` did not return a callTracer frame; the RPC endpoint may not \
364                     support the `callTracer`"
365                );
366            };
367
368            let success = frame.error.is_none() && frame.revert_reason.is_none();
369            let gas_used = frame.gas_used.saturating_to();
370            let arena = SparsedTraceArena {
371                arena: call_frame_to_arena(&frame),
372                ignored: Default::default(),
373            };
374            let result = TraceResult {
375                success,
376                traces: Some(vec![(TraceKind::Execution, arena)]),
377                gas_used,
378            };
379
380            // Local-artifact labeling matches deployed runtime bytecode against the
381            // project artifacts. There is no local executor on this path, so fetch the code
382            // over RPC for the addresses in the trace. Skip the extra round-trips unless
383            // local artifacts were requested.
384            let contracts_bytecode = if with_local_artifacts {
385                let mut contracts_bytecode =
386                    fetch_contracts_bytecode_via_rpc(&provider, &result, block).await?;
387                // The trace ran the override code, not the on-chain code, so the override
388                // wins for artifact matching.
389                contracts_bytecode.extend(override_bytecode);
390                contracts_bytecode
391            } else {
392                Default::default()
393            };
394
395            let chain = alloy_chains::Chain::from_id(provider.get_chain_id().await?);
396            handle_traces(
397                result,
398                &config,
399                chain,
400                &contracts_bytecode,
401                &tracing,
402                with_local_artifacts,
403                false,
404                None,
405            )
406            .await?;
407
408            return Ok(());
409        }
410
411        if trace {
412            if let Some(BlockId::Number(BlockNumberOrTag::Number(block_number))) = self.block {
413                // Override Config `fork_block_number` (if set) with CLI value.
414                config.fork_block_number = Some(block_number);
415            }
416
417            let create2_deployer = evm_opts.create2_deployer;
418            let verbosity = tracing.verbosity;
419            let (mut evm_env, tx_env, fork, chain, networks) =
420                TracingExecutor::<FEN>::get_fork_material(&mut config, evm_opts).await?;
421
422            // modify settings that usually set in eth_call
423            evm_env.cfg_env.disable_block_gas_limit = true;
424            evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
425            evm_env.block_env.set_gas_limit(u64::MAX);
426
427            // Apply the block overrides.
428            if let Some(block_overrides) = block_overrides {
429                if let Some(number) = block_overrides.number {
430                    evm_env.block_env.set_number(number.to());
431                }
432                if let Some(time) = block_overrides.time {
433                    evm_env.block_env.set_timestamp(U256::from(time));
434                }
435            }
436
437            let trace_requirements = TraceRequirements::none()
438                .with_calls(true)
439                .with_debug(debug)
440                .with_decode_internal(if tracing.decode_internal {
441                    InternalTraceMode::Full
442                } else {
443                    InternalTraceMode::None
444                })
445                .with_state_changes(verbosity > 4);
446            let mut executor = TracingExecutor::<FEN>::new(
447                (evm_env, tx_env),
448                fork,
449                evm_version,
450                trace_requirements,
451                networks,
452                create2_deployer,
453                state_overrides,
454            )?;
455
456            let value = tx.value().unwrap_or_default();
457            let input = tx.input().cloned().unwrap_or_default();
458            let tx_kind = tx.kind().expect("set by builder");
459
460            // Apply a user-provided `--gas-limit` to the executor. `build_test_env` propagates the
461            // executor's gas limit to the executed call/deploy, so setting it here is what takes
462            // effect; writing it onto the tx env directly would be overwritten. When no limit is
463            // given, the executor keeps the block gas limit (`u64::MAX`) set above.
464            if let Some(gas_limit) = tx.gas_limit() {
465                executor.set_gas_limit(gas_limit);
466            }
467
468            let env_tx = executor.tx_env_mut();
469
470            // Set transaction options with --trace
471            if let Some(gas_price) = tx.gas_price() {
472                env_tx.set_gas_price(gas_price);
473            }
474
475            if let Some(max_fee_per_gas) = tx.max_fee_per_gas() {
476                env_tx.set_gas_price(max_fee_per_gas);
477            }
478
479            if let Some(max_priority_fee_per_gas) = tx.max_priority_fee_per_gas() {
480                env_tx.set_gas_priority_fee(Some(max_priority_fee_per_gas));
481            }
482
483            if let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() {
484                env_tx.set_max_fee_per_blob_gas(max_fee_per_blob_gas);
485            }
486
487            if let Some(nonce) = tx.nonce() {
488                env_tx.set_nonce(nonce);
489            }
490
491            env_tx.set_tx_type(tx.output_tx_type().into());
492
493            if let Some(access_list) = tx.access_list().cloned() {
494                env_tx.set_access_list(access_list);
495            }
496
497            if let Some(auth) = tx.authorization_list().cloned() {
498                env_tx.set_signed_authorization(auth);
499            }
500
501            let trace = match tx_kind {
502                TxKind::Create => {
503                    let deploy_result = executor.deploy(from, input, value, None);
504                    TraceResult::try_from(deploy_result)?
505                }
506                TxKind::Call(to) => TraceResult::from_raw(
507                    executor.transact_raw(from, to, input, value)?,
508                    TraceKind::Execution,
509                ),
510            };
511
512            let contracts_bytecode = fetch_contracts_bytecode_from_trace(&executor, &trace)?;
513            handle_traces(
514                trace,
515                &config,
516                chain,
517                &contracts_bytecode,
518                &tracing,
519                with_local_artifacts,
520                debug,
521                None,
522            )
523            .await?;
524
525            return Ok(());
526        }
527
528        let response = Cast::new(&provider)
529            .call(&tx, func.as_ref(), block, state_overrides, block_overrides)
530            .await?;
531
532        if response == "0x"
533            && let Some(contract_address) = tx.to()
534        {
535            let code = provider.get_code_at(contract_address).await?;
536            if code.is_empty() {
537                sh_warn!("Contract code is empty")?;
538            }
539        }
540
541        sh_println!("{}", response)?;
542
543        Ok(())
544    }
545
546    /// Handle --curl mode by generating curl command without any RPC interaction.
547    async fn run_curl(self) -> Result<()> {
548        let config = self.rpc.load_config()?;
549        let url = config.get_rpc_url_or_localhost_http()?;
550        let jwt = config.get_rpc_jwt_secret()?;
551
552        // Get call data - either from --data or from sig + args
553        let data = if let Some(data) = &self.data {
554            hex::decode(data)?
555        } else if let Some(sig) = &self.sig {
556            // If sig is already hex data, use it directly
557            if let Ok(data) = hex::decode(sig) {
558                data
559            } else {
560                // Parse function signature and encode args
561                let func = get_func(sig)?;
562                encode_function_args(&func, &self.args)?
563            }
564        } else {
565            Vec::new()
566        };
567
568        // Resolve the destination address (must be a raw address for curl mode)
569        let to = self.to.as_ref().map(|n| match n {
570            NameOrAddress::Address(addr) => Ok(*addr),
571            NameOrAddress::Name(name) => {
572                eyre::bail!("ENS names are not supported with --curl. Please use a raw address instead of '{}'", name);
573            }
574        }).transpose()?;
575
576        // Build eth_call params. `--curl` builds the request offline, so the fields the
577        // RPC-backed builder would resolve against the node (fee style, blob sidecars,
578        // authorization lists) are left to the node's defaults; the scalar fields given on the
579        // command line are forwarded as-is so the printed request runs the same call as the
580        // non-curl command.
581        let mut call_object = serde_json::json!({
582            "to": to,
583            "data": format!("0x{}", hex::encode(&data)),
584        });
585        if let Some(from) = self.wallet.from {
586            call_object["from"] = serde_json::json!(from);
587        }
588        if let Some(value) = self.tx.value {
589            call_object["value"] = serde_json::json!(value);
590        }
591        if let Some(gas_limit) = self.tx.gas_limit {
592            call_object["gas"] = serde_json::json!(gas_limit);
593        }
594        if let Some(nonce) = self.tx.nonce {
595            call_object["nonce"] = serde_json::json!(nonce);
596        }
597
598        let block_param = self
599            .block
600            .map(|b| serde_json::to_value(b).unwrap_or(serde_json::json!("latest")))
601            .unwrap_or(serde_json::json!("latest"));
602
603        // `--debug-trace-call` fetches a callTracer trace of the call instead of executing it,
604        // so the curl payload must target `debug_traceCall` with the same third param as the
605        // non-curl path: the tracer options plus any state / block overrides, so the printed
606        // request traces the same state as the command it represents.
607        let (method, params) = if self.debug_trace_call {
608            let mut call_options = GethDebugTracingCallOptions::default().with_tracing_options(
609                GethDebugTracingOptions::default()
610                    .with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
611                    .with_call_config(CallConfig::default().with_log()),
612            );
613            if let Some(state_overrides) = self.get_state_overrides()? {
614                call_options = call_options.with_state_overrides(state_overrides);
615            }
616            if let Some(block_overrides) = self.get_block_overrides()? {
617                call_options = call_options.with_block_overrides(block_overrides);
618            }
619            ("debug_traceCall", serde_json::json!([call_object, block_param, call_options]))
620        } else {
621            ("eth_call", serde_json::json!([call_object, block_param]))
622        };
623
624        let curl_cmd = generate_curl_command(
625            url.as_ref(),
626            method,
627            params,
628            config.eth_rpc_headers.as_deref(),
629            jwt.as_deref(),
630        )?;
631
632        sh_println!("{}", curl_cmd)?;
633        Ok(())
634    }
635
636    /// Parses state overrides from command line arguments.
637    pub fn get_state_overrides(&self) -> Result<Option<StateOverride>> {
638        self.overrides.get_state_overrides()
639    }
640
641    /// Parses block overrides from command line arguments.
642    pub fn get_block_overrides(&self) -> Result<Option<BlockOverrides>> {
643        self.overrides.get_block_overrides()
644    }
645}
646
647impl figment::Provider for CallArgs {
648    fn metadata(&self) -> Metadata {
649        Metadata::named("CallArgs")
650    }
651
652    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
653        let mut map = Map::new();
654
655        if let Some(evm_version) = self.evm_version {
656            map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
657        }
658        if let Some(chain) = self.chain {
659            map.insert("chain_id".into(), chain.id().into());
660        }
661
662        Ok(Map::from([(Config::selected_profile(), map)]))
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use alloy_primitives::U64;
670
671    #[test]
672    fn can_parse_call_data() {
673        let data = hex::encode("hello");
674        let args = CallArgs::parse_from(["foundry-cli", "--data", data.as_str()]);
675        assert_eq!(args.data, Some(data));
676
677        let data = hex::encode_prefixed("hello");
678        let args = CallArgs::parse_from(["foundry-cli", "--data", data.as_str()]);
679        assert_eq!(args.data, Some(data));
680    }
681
682    #[test]
683    fn chain_is_merged_into_config() {
684        let args = CallArgs::parse_from(["foundry-cli", "--chain", "1"]);
685        let config = Config::from_provider(Config::figment().merge(&args)).unwrap();
686
687        assert_eq!(config.chain, Some(Chain::mainnet()));
688    }
689
690    #[test]
691    fn can_parse_state_overrides() {
692        let args = CallArgs::parse_from([
693            "foundry-cli",
694            "--override-balance",
695            "0x123:0x1234",
696            "--override-nonce",
697            "0x123:1",
698            "--override-code",
699            "0x123:0x1234",
700            "--override-state",
701            "0x123:0x1:0x1234",
702        ]);
703
704        assert_eq!(args.overrides.balance_overrides, Some(vec!["0x123:0x1234".to_string()]));
705        assert_eq!(args.overrides.nonce_overrides, Some(vec!["0x123:1".to_string()]));
706        assert_eq!(args.overrides.code_overrides, Some(vec!["0x123:0x1234".to_string()]));
707        assert_eq!(args.overrides.state_overrides, Some(vec!["0x123:0x1:0x1234".to_string()]));
708    }
709
710    #[test]
711    fn can_parse_multiple_state_overrides() {
712        let args = CallArgs::parse_from([
713            "foundry-cli",
714            "--override-balance",
715            "0x123:0x1234",
716            "--override-balance",
717            "0x456:0x5678",
718            "--override-nonce",
719            "0x123:1",
720            "--override-nonce",
721            "0x456:2",
722            "--override-code",
723            "0x123:0x1234",
724            "--override-code",
725            "0x456:0x5678",
726            "--override-state",
727            "0x123:0x1:0x1234",
728            "--override-state",
729            "0x456:0x2:0x5678",
730        ]);
731
732        assert_eq!(
733            args.overrides.balance_overrides,
734            Some(vec!["0x123:0x1234".to_string(), "0x456:0x5678".to_string()])
735        );
736        assert_eq!(
737            args.overrides.nonce_overrides,
738            Some(vec!["0x123:1".to_string(), "0x456:2".to_string()])
739        );
740        assert_eq!(
741            args.overrides.code_overrides,
742            Some(vec!["0x123:0x1234".to_string(), "0x456:0x5678".to_string()])
743        );
744        assert_eq!(
745            args.overrides.state_overrides,
746            Some(vec!["0x123:0x1:0x1234".to_string(), "0x456:0x2:0x5678".to_string()])
747        );
748    }
749
750    #[test]
751    fn test_negative_args_with_flags() {
752        // Test that negative args work with flags
753        let args = CallArgs::parse_from([
754            "foundry-cli",
755            "--trace",
756            "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
757            "process(int256)",
758            "-999999",
759            "--debug",
760        ]);
761
762        assert!(args.trace);
763        assert!(args.debug);
764        assert_eq!(args.args, vec!["-999999"]);
765    }
766
767    #[test]
768    fn test_transaction_opts_with_trace() {
769        // Test that transaction options are correctly parsed when using --trace
770        let args = CallArgs::parse_from([
771            "foundry-cli",
772            "--trace",
773            "--gas-limit",
774            "1000000",
775            "--gas-price",
776            "20000000000",
777            "--priority-gas-price",
778            "2000000000",
779            "--nonce",
780            "42",
781            "--value",
782            "1000000000000000000", // 1 ETH
783            "--blob-gas-price",
784            "10000000000",
785            "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
786            "balanceOf(address)",
787            "0x123456789abcdef123456789abcdef123456789a",
788        ]);
789
790        assert!(args.trace);
791        assert_eq!(args.tx.gas_limit, Some(U256::from(1000000u32)));
792        assert_eq!(args.tx.gas_price, Some(U256::from(20000000000u64)));
793        assert_eq!(args.tx.priority_gas_price, Some(U256::from(2000000000u64)));
794        assert_eq!(args.tx.nonce, Some(U64::from(42)));
795        assert_eq!(args.tx.value, Some(U256::from(1000000000000000000u64)));
796        assert_eq!(args.tx.blob_gas_price, Some(U256::from(10000000000u64)));
797    }
798
799    #[test]
800    fn debug_trace_call_conflicts_with_trace() {
801        let result = CallArgs::try_parse_from(["foundry-cli", "--trace", "--debug-trace-call"]);
802        assert!(result.is_err(), "--trace and --debug-trace-call must be mutually exclusive");
803    }
804
805    #[test]
806    fn debug_trace_call_rejects_local_trace_flags() {
807        for flag in ["--debug", "--decode-internal"] {
808            let result = CallArgs::try_parse_from([
809                "foundry-cli",
810                "--debug-trace-call",
811                "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
812                flag,
813            ]);
814            assert!(result.is_err(), "--debug-trace-call must reject {flag}");
815        }
816        // --evm-version takes a value, so it is checked separately from the boolean flags above.
817        let result = CallArgs::try_parse_from([
818            "foundry-cli",
819            "--debug-trace-call",
820            "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
821            "--evm-version",
822            "shanghai",
823        ]);
824        assert!(result.is_err(), "--debug-trace-call must reject --evm-version");
825    }
826
827    #[test]
828    fn debug_trace_call_ignores_configured_internal_decoding() {
829        let args = CallArgs::parse_from(["foundry-cli", "--debug-trace-call"]);
830        let config = TracingConfig { decode_internal: true, ..Default::default() };
831
832        assert!(!args.resolve_tracing(&config, 0).decode_internal);
833    }
834}