Skip to main content

cast/cmd/
call.rs

1use super::{
2    auth::{confirm_auth_rpc_disclosure, confirm_auth_rpc_disclosure_before_network_resolution},
3    call_overrides::CallOverrideOpts,
4    run::{fetch_contracts_bytecode_from_trace, fetch_contracts_bytecode_via_rpc},
5};
6use crate::{
7    Cast,
8    debug::{ensure_remote_trace_context_unchanged, handle_traces, select_remote_trace_hardfork},
9    rpc_trace::{call_frame_to_arena, is_method_not_found_error, is_missing_state_error},
10    traces::TraceKind,
11    tx::{CastTxBuilder, SenderKind},
12};
13use alloy_consensus::BlockHeader;
14use alloy_eips::BlockNumHash;
15use alloy_ens::NameOrAddress;
16use alloy_network::{
17    BlockResponse, Network, NetworkTransactionBuilder, TransactionBuilder,
18    primitives::HeaderResponse,
19};
20use alloy_primitives::{B256, Bytes, TxKind, U256, hex, map::AddressHashMap};
21use alloy_provider::{Provider, ext::DebugApi};
22use alloy_rpc_types::{
23    BlockId, BlockNumberOrTag, BlockOverrides,
24    state::StateOverride,
25    trace::geth::{
26        CallConfig, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions,
27        GethDebugTracingOptions, GethTrace,
28    },
29};
30use clap::Parser;
31use eyre::Result;
32use foundry_cli::{
33    opts::{ChainValueParser, RpcOpts, TracingArgs, TransactionOpts},
34    utils::{LoadConfig, TraceResult, parse_ether_value},
35};
36use foundry_common::{
37    FoundryTransactionBuilder,
38    abi::{encode_function_args, get_func},
39    provider::{ProviderBuilder, curl_transport::generate_curl_command},
40    sh_println, shell,
41};
42use foundry_compilers::artifacts::EvmVersion;
43use foundry_config::{
44    Chain, Config, FoundryHardfork, TracingConfig,
45    figment::{
46        self, Metadata, Profile,
47        value::{Dict, Map},
48    },
49};
50#[cfg(feature = "optimism")]
51use foundry_evm::core::evm::OpEvmNetwork;
52use foundry_evm::{
53    core::{
54        FoundryBlock, FoundryTransaction,
55        evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, context_for_child_transaction},
56    },
57    executors::TracingExecutor,
58    opts::EvmOpts,
59    traces::{InternalTraceMode, SparsedTraceArena, TraceRequirements},
60};
61use foundry_evm_networks::NetworkConfigs;
62use foundry_wallets::{BrowserWalletOpts, WalletOpts};
63use revm::context::Block;
64use std::str::FromStr;
65
66/// CLI arguments for `cast call`.
67///
68/// ## State Override Flags
69///
70/// The following flags can be used to override the state for the call:
71///
72/// * `--override-balance <address>:<balance>` - Override the balance of an account
73/// * `--override-nonce <address>:<nonce>` - Override the nonce of an account
74/// * `--override-code <address>:<code>` - Override the code of an account
75/// * `--override-state <address>:<slot>:<value>` - Override a storage slot of an account
76///
77/// Multiple overrides can be specified for the same account. For example:
78///
79/// ```bash
80/// cast call 0x... "transfer(address,uint256)" 0x... 100 \
81///   --override-balance 0x123:0x1234 \
82///   --override-nonce 0x123:1 \
83///   --override-code 0x123:0x1234 \
84///   --override-state 0x123:0x1:0x1234
85///   --override-state-diff 0x123:0x1:0x1234
86/// ```
87///
88/// `--delegate` builds on the same mechanism: it overrides the code of the `--from` address with
89/// the destination's code so the call runs as a `delegatecall`.
90#[derive(Debug, Parser)]
91pub struct CallArgs {
92    /// The destination of the transaction.
93    #[arg(value_parser = NameOrAddress::from_str)]
94    to: Option<NameOrAddress>,
95
96    /// The signature of the function to call.
97    sig: Option<String>,
98
99    /// The arguments of the function to call.
100    #[arg(allow_negative_numbers = true)]
101    args: Vec<String>,
102
103    /// Raw hex-encoded data for the transaction. Used instead of `SIG` and `ARGS`.
104    #[arg(
105        long,
106        conflicts_with_all = &["sig", "args"]
107    )]
108    data: Option<String>,
109
110    /// Forks the remote rpc, executes the transaction locally and prints a trace
111    #[arg(long, default_value_t = false)]
112    trace: bool,
113
114    /// Simulate the call as a `delegatecall` from the `--from` address.
115    ///
116    /// The destination's runtime code is applied as a code override on the `--from` address and
117    /// the call is then made to that address, so the destination's code runs in the caller's
118    /// storage context, like an on-chain `delegatecall`.
119    ///
120    /// Note that the executed code observes `msg.sender` (and `tx.origin`) equal to the `--from`
121    /// address itself, whereas in an on-chain `delegatecall` `msg.sender` is preserved from the
122    /// delegating contract's own caller.
123    #[arg(long, requires = "from", conflicts_with = "browser")]
124    delegate: bool,
125
126    /// Fetch the call trace from the node via `debug_traceCall` (callTracer) and render it,
127    /// instead of re-executing the call locally like `--trace`.
128    ///
129    /// This is a call-tree view: nested calls, value, gas, emitted logs and revert data. It does
130    /// not provide the opcode / struct-log level detail of a local `--trace` / `--debug` run.
131    ///
132    /// The local-execution-only trace flags (`--debug`, `--decode-internal`, `--evm-version`) do
133    /// not apply, since the trace comes from the node rather than a local run.
134    #[arg(
135        long = "debug-trace-call",
136        default_value_t = false,
137        conflicts_with_all = ["trace", "debug", "decode_internal", "evm_version"]
138    )]
139    debug_trace_call: bool,
140
141    /// Opens an interactive debugger.
142    /// Can only be used with `--trace`.
143    #[arg(long, requires = "trace")]
144    debug: bool,
145
146    #[command(flatten)]
147    tracing: TracingArgs,
148
149    /// The EVM Version to use.
150    /// Can only be used with `--trace`.
151    #[arg(long, requires = "trace")]
152    evm_version: Option<EvmVersion>,
153
154    /// The block height to query at.
155    ///
156    /// Can also be the tags earliest, finalized, safe, latest, or pending.
157    #[arg(long, short)]
158    block: Option<BlockId>,
159
160    #[command(subcommand)]
161    command: Option<CallSubcommands>,
162
163    #[command(flatten)]
164    tx: TransactionOpts,
165
166    /// Skip the EIP-7702 authorization disclosure confirmation.
167    #[arg(long)]
168    force: bool,
169
170    #[command(flatten)]
171    rpc: RpcOpts,
172
173    #[command(flatten)]
174    wallet: WalletOpts,
175
176    #[command(flatten)]
177    browser: BrowserWalletOpts,
178
179    #[arg(
180        short,
181        long,
182        alias = "chain-id",
183        env = "CHAIN",
184        value_parser = ChainValueParser::default(),
185    )]
186    pub chain: Option<Chain>,
187
188    /// Use current project artifacts for trace decoding.
189    #[arg(long, visible_alias = "la")]
190    pub with_local_artifacts: bool,
191
192    #[command(flatten)]
193    pub overrides: CallOverrideOpts,
194}
195
196#[derive(Debug, Parser)]
197pub enum CallSubcommands {
198    /// ignores the address field and simulates creating a contract
199    #[command(name = "--create")]
200    Create {
201        /// Bytecode of contract.
202        code: String,
203
204        /// The signature of the constructor.
205        sig: Option<String>,
206
207        /// The arguments of the constructor.
208        #[arg(allow_negative_numbers = true)]
209        args: Vec<String>,
210
211        /// Ether to send in the transaction.
212        ///
213        /// Either specified in wei, or as a string with a unit type.
214        ///
215        /// Examples: 1ether, 10gwei, 0.01ether
216        #[arg(long, value_parser = parse_ether_value)]
217        value: Option<U256>,
218    },
219}
220
221struct AuthDisclosurePreflight {
222    confirmed: bool,
223    sender: Option<SenderKind<'static>>,
224}
225
226fn infer_network_from_chain_id(networks: NetworkConfigs, chain_id: u64) -> Result<NetworkConfigs> {
227    networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)
228}
229
230impl CallArgs {
231    fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
232        if self.debug_trace_call {
233            self.tracing.resolve_call_tracer(config, verbosity)
234        } else {
235            self.tracing.resolve(config, verbosity)
236        }
237    }
238
239    pub async fn run(mut self) -> Result<()> {
240        self.validate_trace_args()?;
241
242        // Handle --curl mode early, before any provider interaction
243        if self.rpc.curl {
244            if self.browser.browser {
245                eyre::bail!("--browser cannot be combined with --curl; use --from <ADDRESS>");
246            }
247            if self.delegate {
248                // The code override that makes the call a `delegatecall` is read from the node,
249                // which `--curl` deliberately never contacts.
250                eyre::bail!("--delegate cannot be combined with --curl");
251            }
252            return self.run_curl().await;
253        }
254
255        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
256        let (mut config, mut evm_opts) = super::load_cast_config_and_evm_opts(figment)?;
257        evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned());
258        if self.tx.tempo.is_tempo() {
259            evm_opts.networks = NetworkConfigs::with_tempo();
260        } else if let Some(chain) = self.chain {
261            evm_opts.networks = infer_network_from_chain_id(evm_opts.networks, chain.id())?;
262        }
263        let Some(auth_preflight) = self.preflight_auth_disclosure().await? else {
264            return Ok(());
265        };
266        evm_opts.infer_network_from_fork().await?;
267        if self.chain.is_none()
268            && let Some(chain_id) = evm_opts.env.chain_id
269        {
270            let chain = Chain::from_id(chain_id);
271            self.chain = Some(chain);
272            config.chain = Some(chain);
273        }
274
275        if evm_opts.networks.is_tempo() {
276            return self
277                .run_with_network_and_opts::<TempoEvmNetwork>(config, evm_opts, auth_preflight)
278                .await;
279        }
280
281        #[cfg(feature = "monad")]
282        if evm_opts.networks.is_monad() {
283            return self
284                .run_with_network_and_opts::<foundry_evm::core::evm::MonadEvmNetwork>(
285                    config,
286                    evm_opts,
287                    auth_preflight,
288                )
289                .await;
290        }
291
292        #[cfg(feature = "optimism")]
293        if evm_opts.networks.is_optimism() {
294            return self
295                .run_with_network_and_opts::<OpEvmNetwork>(config, evm_opts, auth_preflight)
296                .await;
297        }
298
299        self.run_with_network_and_opts::<EthEvmNetwork>(config, evm_opts, auth_preflight).await
300    }
301
302    /// Returns whether resolving this call can disclose an authorization before the transaction
303    /// builder exists. This mirrors the builder's disclosure check after applying `.raw()`.
304    const fn will_disclose_auth(&self) -> bool {
305        !self.tx.auth.is_empty() && (!self.trace || matches!(self.tx.access_list, Some(None)))
306    }
307
308    async fn preflight_auth_disclosure(&self) -> Result<Option<AuthDisclosurePreflight>> {
309        if !self.will_disclose_auth() {
310            return Ok(Some(AuthDisclosurePreflight { confirmed: false, sender: None }));
311        }
312
313        let sender = if self.browser.browser {
314            None
315        } else {
316            Some(SenderKind::from_wallet_opts(self.wallet.clone()).await?)
317        };
318        let browser_sender = SenderKind::from(self.wallet.from.unwrap_or_default());
319        let validation_sender = sender.as_ref().unwrap_or(&browser_sender);
320        if !confirm_auth_rpc_disclosure_before_network_resolution(
321            &self.tx.auth,
322            validation_sender,
323            self.force,
324        )? {
325            return Ok(None);
326        }
327
328        Ok(Some(AuthDisclosurePreflight { confirmed: true, sender }))
329    }
330
331    fn validate_trace_args(&self) -> Result<()> {
332        if !self.trace
333            && !self.debug_trace_call
334            && (self.tracing.disable_labels
335                || self.tracing.compact_labels
336                || !self.tracing.labels.is_empty()
337                || self.tracing.trace_depth.is_some())
338        {
339            eyre::bail!("trace rendering options require `--trace` or `--debug-trace-call`");
340        }
341
342        if self.tracing.decode_internal && !self.trace {
343            eyre::bail!("`--decode-internal` requires `--trace`");
344        }
345
346        Ok(())
347    }
348
349    async fn run_with_network_and_opts<FEN: FoundryEvmNetwork>(
350        self,
351        mut config: Box<Config>,
352        evm_opts: EvmOpts,
353        auth_preflight: AuthDisclosurePreflight,
354    ) -> Result<()>
355    where
356        <FEN::Network as Network>::TransactionRequest: FoundryTransactionBuilder<FEN::Network>,
357    {
358        config.networks = evm_opts.networks;
359        let mut state_overrides = self.get_state_overrides()?;
360        let block_overrides = self.get_block_overrides()?;
361        config.tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
362        let tracing = config.tracing.clone();
363
364        let Self {
365            mut to,
366            mut sig,
367            mut args,
368            mut tx,
369            command,
370            block,
371            trace,
372            debug_trace_call,
373            evm_version,
374            debug,
375            data,
376            with_local_artifacts,
377            wallet,
378            browser,
379            force,
380            delegate,
381            ..
382        } = self;
383
384        if let Some(data) = data {
385            sig = Some(data);
386        }
387
388        let provider = ProviderBuilder::<FEN::Network>::from_config(&config)?.build()?;
389        let endpoint_identity =
390            if debug_trace_call { Some(evm_opts.discover_fork_endpoint().await?) } else { None };
391        let sender = if let Some(sender) = auth_preflight.sender {
392            sender
393        } else if let Some(browser) = browser.run::<FEN::Network>().await? {
394            browser.address().into()
395        } else {
396            SenderKind::from_wallet_opts(wallet).await?
397        };
398        let from = sender.address();
399
400        // A `delegatecall` runs the destination's code against the caller's storage, which
401        // `eth_call` cannot express. Overriding the caller's code with the destination's and
402        // then calling the caller reproduces that context. The retarget to the caller happens
403        // after the transaction is built, so calldata encoding and function resolution still
404        // see the destination.
405        if delegate {
406            if command.is_some() {
407                eyre::bail!("`--delegate` cannot be combined with `--create`");
408            }
409            let Some(target) = to else {
410                eyre::bail!("`--delegate` requires a destination address");
411            };
412            let target = target.resolve(&provider).await?;
413            let overrides = state_overrides.get_or_insert_with(Default::default);
414            if overrides.get(&from).is_some_and(|account| account.code.is_some()) {
415                eyre::bail!("`--delegate` conflicts with `--override-code` for the sender {from}");
416            }
417            // A code override for the destination is the state this call runs against, so it
418            // takes precedence over the deployed code.
419            let code = match overrides.get(&target).and_then(|account| account.code.clone()) {
420                Some(code) => code,
421                None => provider.get_code_at(target).block_id(block.unwrap_or_default()).await?,
422            };
423            if code.is_empty() {
424                eyre::bail!("`--delegate` destination {target} has no code to delegate to");
425            }
426            overrides.entry(from).or_default().code = Some(code);
427            to = Some(NameOrAddress::Address(target));
428        }
429
430        let code = if let Some(CallSubcommands::Create {
431            code,
432            sig: create_sig,
433            args: create_args,
434            value,
435        }) = command
436        {
437            sig = create_sig;
438            args = create_args;
439            if let Some(value) = value {
440                tx.value = Some(value);
441            }
442            Some(code)
443        } else {
444            None
445        };
446
447        let builder = CastTxBuilder::new(&provider, tx, &config)
448            .await?
449            .with_to(to)
450            .await?
451            .with_code_sig_and_args(code, sig, args)
452            .await?
453            .raw();
454        let will_disclose =
455            (!trace && builder.has_auth()) || builder.will_disclose_auth_during_build();
456        if will_disclose
457            && !auth_preflight.confirmed
458            && !confirm_auth_rpc_disclosure(&builder, &sender, force)?
459        {
460            return Ok(());
461        }
462        let (mut tx, func) = builder.build(sender).await?;
463
464        // The delegate override put the destination's code on the sender, so the built call is
465        // aimed at the sender; the calldata above was still encoded against the destination.
466        if delegate {
467            tx.set_to(from);
468        }
469
470        if debug_trace_call {
471            let endpoint_identity = endpoint_identity
472                .as_ref()
473                .ok_or_else(|| eyre::eyre!("remote trace endpoint identity was not captured"))?;
474            let requested_block = block.unwrap_or(BlockId::latest());
475            let fetched_block = provider.get_block(requested_block).await?;
476            let resolved_canonical_block =
477                if matches!(requested_block, BlockId::Number(_)) && !requested_block.is_pending() {
478                    fetched_block.as_ref().map(|block| {
479                        BlockNumHash::new(block.header().number(), block.header().hash())
480                    })
481                } else {
482                    None
483                };
484            let block = pin_remote_trace_block(
485                requested_block,
486                fetched_block.as_ref().map(|block| block.header().hash()),
487            )?;
488            let block_time_override = block_overrides.as_ref().and_then(|overrides| overrides.time);
489            let mut call_options = GethDebugTracingCallOptions::default().with_tracing_options(
490                GethDebugTracingOptions::default()
491                    .with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
492                    .with_call_config(CallConfig::default().with_log()),
493            );
494            // A contract that only exists through a `--override-code` entry has no on-chain
495            // code to fetch for local-artifact matching, so remember the override code before
496            // handing the overrides to `debug_traceCall`.
497            let mut override_bytecode = AddressHashMap::<Bytes>::default();
498            if with_local_artifacts && let Some(overrides) = &state_overrides {
499                for (address, account) in overrides {
500                    if let Some(code) = &account.code {
501                        override_bytecode.insert(*address, code.clone());
502                    }
503                }
504            }
505
506            // Honour the same state / block overrides as the local `--trace` path.
507            if let Some(state_overrides) = state_overrides {
508                call_options = call_options.with_state_overrides(state_overrides);
509            }
510            if let Some(block_overrides) = block_overrides {
511                call_options = call_options.with_block_overrides(block_overrides);
512            }
513
514            let geth_trace = provider
515                .debug_trace_call(tx, block, call_options)
516                .await
517                .map_err(|err| -> eyre::Report {
518                    // Two RPC rejections deserve an actionable hint instead of the raw transport
519                    // error, and they need different fixes: a disabled `debug` namespace, and
520                    // missing historical state, hit whenever `--block` targets a block a full
521                    // node has pruned.
522                    if is_method_not_found_error(&err) {
523                        eyre::eyre!(
524                            "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`"
525                        )
526                    } else if is_missing_state_error(&err) {
527                        eyre::eyre!(
528                            "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`"
529                        )
530                    } else {
531                        err.into()
532                    }
533                })?;
534            let GethTrace::CallTracer(frame) = geth_trace else {
535                eyre::bail!(
536                    "`debug_traceCall` did not return a callTracer frame; the RPC endpoint may not \
537                     support the `callTracer`"
538                );
539            };
540
541            let success = frame.error.is_none() && frame.revert_reason.is_none();
542            let gas_used = frame.gas_used.saturating_to();
543            let arena = SparsedTraceArena {
544                arena: call_frame_to_arena(&frame),
545                ignored: Default::default(),
546                diagnostics: Default::default(),
547            };
548            let result = TraceResult {
549                success,
550                traces: Some(vec![(TraceKind::Execution, arena)]),
551                gas_used,
552            };
553
554            // Local-artifact labeling matches deployed runtime bytecode against the
555            // project artifacts. There is no local executor on this path, so fetch the code
556            // over RPC for the addresses in the trace. Skip the extra round-trips unless
557            // local artifacts were requested.
558            let contracts_bytecode = if with_local_artifacts {
559                let mut contracts_bytecode =
560                    fetch_contracts_bytecode_via_rpc(&provider, &result, block).await?;
561                // The trace ran the override code, not the on-chain code, so the override
562                // wins for artifact matching.
563                contracts_bytecode.extend(override_bytecode);
564                contracts_bytecode
565            } else {
566                Default::default()
567            };
568            let final_endpoint_identity = evm_opts.discover_fork_endpoint().await?;
569            ensure_remote_trace_context_unchanged(endpoint_identity, &final_endpoint_identity)?;
570
571            // The remote node executed this trace, so its reported family is authoritative for
572            // decoding even when the caller selected a compatible local EVM implementation.
573            let execution_network = endpoint_identity.network;
574            let chain = alloy_chains::Chain::from_id(endpoint_identity.source_chain_id);
575            let block_timestamp = if let Some(timestamp) = block_time_override {
576                Some(timestamp)
577            } else {
578                fetched_block.as_ref().map(|block| block.header().timestamp())
579            };
580            // A configured hardfork is an explicit trace-decoding override. Otherwise honor an
581            // Anvil endpoint's exact execution hardfork before consulting the source schedule.
582            let resolved_hardfork = select_remote_trace_hardfork(
583                config.hardfork,
584                endpoint_identity.hardfork,
585                execution_network,
586            )
587            .or_else(|| {
588                block_timestamp.and_then(|timestamp| {
589                    FoundryHardfork::from_chain_and_timestamp(chain.id(), timestamp)
590                })
591            });
592            if let Some(resolved_block) = resolved_canonical_block {
593                let canonical_block =
594                    provider.get_block_by_number(resolved_block.number.into()).await?;
595                ensure_remote_trace_block_is_canonical(
596                    resolved_block,
597                    canonical_block.as_ref().map(|block| {
598                        BlockNumHash::new(block.header().number(), block.header().hash())
599                    }),
600                )?;
601            }
602            handle_traces(
603                result,
604                &config,
605                chain,
606                &contracts_bytecode,
607                &tracing,
608                with_local_artifacts,
609                false,
610                resolved_hardfork,
611                endpoint_identity.network_profile,
612            )
613            .await?;
614
615            return Ok(());
616        }
617
618        if trace {
619            if let Some(BlockId::Number(BlockNumberOrTag::Number(block_number))) = block {
620                // Override Config `fork_block_number` (if set) with CLI value.
621                config.fork_block_number = Some(block_number);
622            }
623
624            let create2_deployer = evm_opts.create2_deployer;
625            let verbosity = tracing.verbosity;
626            let (mut evm_env, tx_env, fork, chain, networks, endpoint_hardfork) =
627                TracingExecutor::<FEN>::get_fork_material(&mut config, evm_opts).await?;
628            let context_block_number = evm_env.block_env.number().saturating_to();
629            // Modify settings usually set in eth_call while keeping execution gas bounded.
630            evm_env.cfg_env.disable_block_gas_limit = true;
631            evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
632
633            // Apply the block overrides.
634            if let Some(block_overrides) = block_overrides {
635                if let Some(number) = block_overrides.number {
636                    evm_env.block_env.set_number(number.to());
637                }
638                if let Some(time) = block_overrides.time {
639                    evm_env.block_env.set_timestamp(U256::from(time));
640                }
641            }
642            let resolved_hardfork = TracingExecutor::<FEN>::resolve_spec_for_chain(
643                &config,
644                networks,
645                chain.id(),
646                endpoint_hardfork,
647                &mut evm_env,
648                evm_version,
649            );
650            TracingExecutor::<FEN>::extend_precompile_labels(
651                &mut config,
652                networks,
653                resolved_hardfork,
654            );
655
656            let trace_requirements = TraceRequirements::none()
657                .with_calls(true)
658                .with_debug(debug)
659                .with_decode_internal(if tracing.decode_internal {
660                    InternalTraceMode::Full
661                } else {
662                    InternalTraceMode::None
663                })
664                .with_state_changes(verbosity > 4);
665            let mut executor = TracingExecutor::<FEN>::new(
666                (evm_env, tx_env),
667                fork,
668                None,
669                trace_requirements,
670                networks,
671                create2_deployer,
672                state_overrides,
673            )?;
674
675            let value = tx.value().unwrap_or_default();
676            let input = tx.input().cloned().unwrap_or_default();
677            let tx_kind = tx.kind().expect("set by builder");
678
679            // Apply a user-provided `--gas-limit` to the executor. `build_test_env` propagates the
680            // executor's gas limit to the executed call/deploy, so setting it here is what takes
681            // effect; writing it onto the tx env directly would be overwritten.
682            if let Some(gas_limit) = tx.gas_limit() {
683                executor.set_gas_limit(gas_limit);
684            }
685
686            let env_tx = executor.tx_env_mut();
687
688            // Set transaction options with --trace
689            if let Some(gas_price) = tx.gas_price() {
690                env_tx.set_gas_price(gas_price);
691            }
692
693            if let Some(max_fee_per_gas) = tx.max_fee_per_gas() {
694                env_tx.set_gas_price(max_fee_per_gas);
695            }
696
697            if let Some(max_priority_fee_per_gas) = tx.max_priority_fee_per_gas() {
698                env_tx.set_gas_priority_fee(Some(max_priority_fee_per_gas));
699            }
700
701            if let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() {
702                env_tx.set_max_fee_per_blob_gas(max_fee_per_blob_gas);
703            }
704
705            if let Some(nonce) = tx.nonce() {
706                env_tx.set_nonce(nonce);
707            }
708
709            env_tx.set_tx_type(tx.output_tx_type().into());
710
711            if let Some(access_list) = tx.access_list().cloned() {
712                env_tx.set_access_list(access_list);
713            }
714
715            if let Some(auth) = tx.authorization_list().cloned() {
716                env_tx.set_signed_authorization(auth);
717            }
718
719            let mut context_tx = executor.tx_env().clone();
720            context_tx.set_caller(from);
721            context_tx.set_kind(tx_kind);
722            context_tx.set_data(input.clone());
723            context_tx.set_value(value);
724            let chain_context = context_for_child_transaction::<FEN, _>(
725                &provider,
726                context_block_number,
727                &context_tx,
728                networks,
729            )
730            .await?;
731
732            let trace = match tx_kind {
733                TxKind::Create => {
734                    let deploy_result =
735                        executor.deploy_with_context(from, input, value, chain_context, None);
736                    TraceResult::try_from(deploy_result)?
737                }
738                TxKind::Call(to) => TraceResult::from_raw(
739                    executor.transact_raw_with_context(from, to, input, value, chain_context)?,
740                    TraceKind::Execution,
741                ),
742            };
743
744            let contracts_bytecode = fetch_contracts_bytecode_from_trace(&executor, &trace)?;
745            handle_traces(
746                trace,
747                &config,
748                chain,
749                &contracts_bytecode,
750                &tracing,
751                with_local_artifacts,
752                debug,
753                resolved_hardfork,
754                networks,
755            )
756            .await?;
757
758            return Ok(());
759        }
760
761        let response = Cast::new(&provider)
762            .call(&tx, func.as_ref(), block, state_overrides, block_overrides)
763            .await?;
764
765        // With `--delegate` the call targets the sender, whose code comes from the override and
766        // was already checked to be non-empty, so the on-chain code lookup would be misleading.
767        if response == "0x"
768            && !delegate
769            && let Some(contract_address) = tx.to()
770        {
771            let code = provider.get_code_at(contract_address).await?;
772            if code.is_empty() {
773                sh_warn!("Contract code is empty")?;
774            }
775        }
776
777        // Bypass the shell verbosity layer so `--quiet` does not suppress the primary result.
778        let mut shell = shell::Shell::get();
779        let out = shell.out();
780        writeln!(out, "{response}")?;
781        out.flush()?;
782
783        Ok(())
784    }
785
786    /// Handle --curl mode by generating curl command without any RPC interaction.
787    async fn run_curl(self) -> Result<()> {
788        let config = self.rpc.load_config()?;
789        let url = config.get_rpc_url_or_localhost_http()?;
790        let jwt = config.get_rpc_jwt_secret()?;
791
792        // Get call data - either from --data or from sig + args
793        let data = if let Some(data) = &self.data {
794            hex::decode(data)?
795        } else if let Some(sig) = &self.sig {
796            // If sig is already hex data, use it directly
797            if let Ok(data) = hex::decode(sig) {
798                data
799            } else {
800                // Parse function signature and encode args
801                let func = get_func(sig)?;
802                encode_function_args(&func, &self.args)?
803            }
804        } else {
805            Vec::new()
806        };
807
808        // Resolve the destination address (must be a raw address for curl mode)
809        let to = self.to.as_ref().map(|n| match n {
810            NameOrAddress::Address(addr) => Ok(*addr),
811            NameOrAddress::Name(name) => {
812                eyre::bail!("ENS names are not supported with --curl. Please use a raw address instead of '{}'", name);
813            }
814        }).transpose()?;
815
816        // Build eth_call params. `--curl` builds the request offline, so the fields the
817        // RPC-backed builder would resolve against the node (fee style, blob sidecars,
818        // authorization lists) are left to the node's defaults; the scalar fields given on the
819        // command line are forwarded as-is so the printed request runs the same call as the
820        // non-curl command.
821        let mut call_object = serde_json::json!({
822            "to": to,
823            "data": format!("0x{}", hex::encode(&data)),
824        });
825        if let Some(from) = self.wallet.from {
826            call_object["from"] = serde_json::json!(from);
827        }
828        if let Some(value) = self.tx.value {
829            call_object["value"] = serde_json::json!(value);
830        }
831        if let Some(gas_limit) = self.tx.gas_limit {
832            call_object["gas"] = serde_json::json!(gas_limit);
833        }
834        if let Some(nonce) = self.tx.nonce {
835            call_object["nonce"] = serde_json::json!(nonce);
836        }
837
838        let block_param = self
839            .block
840            .map(|b| serde_json::to_value(b).unwrap_or(serde_json::json!("latest")))
841            .unwrap_or(serde_json::json!("latest"));
842
843        // `--debug-trace-call` fetches a callTracer trace of the call instead of executing it,
844        // so the curl payload must target `debug_traceCall` with the same third param as the
845        // non-curl path: the tracer options plus any state / block overrides, so the printed
846        // request traces the same state as the command it represents.
847        let (method, params) = if self.debug_trace_call {
848            let mut call_options = GethDebugTracingCallOptions::default().with_tracing_options(
849                GethDebugTracingOptions::default()
850                    .with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
851                    .with_call_config(CallConfig::default().with_log()),
852            );
853            if let Some(state_overrides) = self.get_state_overrides()? {
854                call_options = call_options.with_state_overrides(state_overrides);
855            }
856            if let Some(block_overrides) = self.get_block_overrides()? {
857                call_options = call_options.with_block_overrides(block_overrides);
858            }
859            ("debug_traceCall", serde_json::json!([call_object, block_param, call_options]))
860        } else {
861            ("eth_call", serde_json::json!([call_object, block_param]))
862        };
863
864        let curl_cmd = generate_curl_command(
865            url.as_ref(),
866            method,
867            params,
868            config.eth_rpc_headers.as_deref(),
869            jwt.as_deref(),
870        )?;
871
872        sh_println!("{}", curl_cmd)?;
873        Ok(())
874    }
875
876    /// Parses state overrides from command line arguments.
877    pub fn get_state_overrides(&self) -> Result<Option<StateOverride>> {
878        self.overrides.get_state_overrides()
879    }
880
881    /// Parses block overrides from command line arguments.
882    pub fn get_block_overrides(&self) -> Result<Option<BlockOverrides>> {
883        self.overrides.get_block_overrides()
884    }
885}
886
887fn pin_remote_trace_block(requested: BlockId, fetched_hash: Option<B256>) -> Result<BlockId> {
888    if requested.is_pending() {
889        return Ok(requested);
890    }
891
892    let fetched_hash = fetched_hash.ok_or_else(|| {
893        eyre::eyre!("block {requested:?} was not found while preparing the remote trace")
894    })?;
895    if let BlockId::Hash(requested_hash) = requested {
896        if requested_hash.block_hash != fetched_hash {
897            eyre::bail!(
898                "the RPC endpoint returned block {fetched_hash} for requested block {}; retry the command",
899                requested_hash.block_hash
900            );
901        }
902        // Preserve `requireCanonical` exactly as supplied by the caller.
903        return Ok(requested);
904    }
905
906    Ok(BlockId::hash(fetched_hash))
907}
908
909fn ensure_remote_trace_block_is_canonical(
910    expected: BlockNumHash,
911    actual: Option<BlockNumHash>,
912) -> Result<()> {
913    let Some(actual) = actual else {
914        eyre::bail!(
915            "block {} at {} changed canonicality while collecting its remote trace: the canonical block lookup no longer reports that height; retry the command",
916            expected.hash,
917            expected.number,
918        );
919    };
920    if actual != expected {
921        eyre::bail!(
922            "block {} at {} changed canonicality while collecting its remote trace: the canonical block lookup reported block {} at {}; retry the command",
923            expected.hash,
924            expected.number,
925            actual.hash,
926            actual.number,
927        );
928    }
929
930    Ok(())
931}
932
933impl figment::Provider for CallArgs {
934    fn metadata(&self) -> Metadata {
935        Metadata::named("CallArgs")
936    }
937
938    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
939        let mut map = Map::new();
940
941        if let Some(evm_version) = self.evm_version {
942            map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
943        }
944        if let Some(chain) = self.chain {
945            map.insert("chain_id".into(), chain.id().into());
946        }
947
948        Ok(Map::from([(Config::selected_profile(), map)]))
949    }
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955    use alloy_eips::RpcBlockHash;
956    use alloy_primitives::U64;
957
958    #[test]
959    fn pending_remote_trace_block_remains_unpinned() {
960        assert_eq!(pin_remote_trace_block(BlockId::pending(), None).unwrap(), BlockId::pending());
961    }
962
963    #[test]
964    fn non_pending_remote_trace_blocks_are_pinned() {
965        let hash = B256::repeat_byte(0x11);
966        for requested in [
967            BlockId::number(42),
968            BlockId::earliest(),
969            BlockId::latest(),
970            BlockId::safe(),
971            BlockId::finalized(),
972        ] {
973            assert_eq!(pin_remote_trace_block(requested, Some(hash)).unwrap(), BlockId::hash(hash));
974        }
975    }
976
977    #[test]
978    fn non_pending_remote_trace_block_must_exist() {
979        let err = pin_remote_trace_block(BlockId::number(42), None).unwrap_err();
980        assert!(err.to_string().contains("was not found while preparing the remote trace"));
981    }
982
983    #[test]
984    fn hash_remote_trace_block_preserves_canonical_requirement() {
985        let hash = B256::repeat_byte(0x22);
986        for require_canonical in [None, Some(false), Some(true)] {
987            let requested = BlockId::Hash(RpcBlockHash { block_hash: hash, require_canonical });
988            assert_eq!(pin_remote_trace_block(requested, Some(hash)).unwrap(), requested);
989        }
990    }
991
992    #[test]
993    fn hash_remote_trace_block_must_match_response() {
994        let err = pin_remote_trace_block(
995            BlockId::hash_canonical(B256::repeat_byte(0x33)),
996            Some(B256::repeat_byte(0x44)),
997        )
998        .unwrap_err();
999        assert!(err.to_string().contains("returned block"));
1000    }
1001
1002    #[test]
1003    fn remote_trace_block_must_remain_canonical() {
1004        let expected = BlockNumHash::new(42, B256::repeat_byte(0x55));
1005
1006        ensure_remote_trace_block_is_canonical(expected, Some(expected)).unwrap();
1007
1008        let err = ensure_remote_trace_block_is_canonical(expected, None).unwrap_err();
1009        assert!(err.to_string().contains("no longer reports that height"));
1010
1011        let err = ensure_remote_trace_block_is_canonical(
1012            expected,
1013            Some(BlockNumHash::new(expected.number, B256::repeat_byte(0x66))),
1014        )
1015        .unwrap_err();
1016        assert!(err.to_string().contains("changed canonicality"));
1017    }
1018
1019    #[test]
1020    fn can_parse_call_data() {
1021        let data = hex::encode("hello");
1022        let args = CallArgs::parse_from(["foundry-cli", "--data", data.as_str()]);
1023        assert_eq!(args.data, Some(data));
1024
1025        let data = hex::encode_prefixed("hello");
1026        let args = CallArgs::parse_from(["foundry-cli", "--data", data.as_str()]);
1027        assert_eq!(args.data, Some(data));
1028    }
1029
1030    #[test]
1031    fn chain_is_merged_into_config() {
1032        let args = CallArgs::parse_from(["foundry-cli", "--chain", "1"]);
1033        let config = Config::from_provider(Config::figment().merge(&args)).unwrap();
1034
1035        assert_eq!(config.chain, Some(Chain::mainnet()));
1036    }
1037
1038    #[test]
1039    #[cfg(not(feature = "monad"))]
1040    fn chain_id_rejects_disabled_monad_network() {
1041        let error = infer_network_from_chain_id(NetworkConfigs::default(), 143).unwrap_err();
1042
1043        assert_eq!(
1044            error.to_string(),
1045            "cannot infer execution network from chain ID 143: network family `monad` is not \
1046             enabled in this build"
1047        );
1048    }
1049
1050    #[test]
1051    fn explicit_ethereum_overrides_chain_id_inference() {
1052        let ethereum = NetworkConfigs::with_ethereum();
1053        let inferred = infer_network_from_chain_id(ethereum, 143).unwrap();
1054
1055        assert_eq!(inferred, ethereum);
1056    }
1057
1058    #[test]
1059    fn can_parse_state_overrides() {
1060        let args = CallArgs::parse_from([
1061            "foundry-cli",
1062            "--override-balance",
1063            "0x123:0x1234",
1064            "--override-nonce",
1065            "0x123:1",
1066            "--override-code",
1067            "0x123:0x1234",
1068            "--override-state",
1069            "0x123:0x1:0x1234",
1070        ]);
1071
1072        assert_eq!(args.overrides.balance_overrides, Some(vec!["0x123:0x1234".to_string()]));
1073        assert_eq!(args.overrides.nonce_overrides, Some(vec!["0x123:1".to_string()]));
1074        assert_eq!(args.overrides.code_overrides, Some(vec!["0x123:0x1234".to_string()]));
1075        assert_eq!(args.overrides.state_overrides, Some(vec!["0x123:0x1:0x1234".to_string()]));
1076    }
1077
1078    #[test]
1079    fn can_parse_multiple_state_overrides() {
1080        let args = CallArgs::parse_from([
1081            "foundry-cli",
1082            "--override-balance",
1083            "0x123:0x1234",
1084            "--override-balance",
1085            "0x456:0x5678",
1086            "--override-nonce",
1087            "0x123:1",
1088            "--override-nonce",
1089            "0x456:2",
1090            "--override-code",
1091            "0x123:0x1234",
1092            "--override-code",
1093            "0x456:0x5678",
1094            "--override-state",
1095            "0x123:0x1:0x1234",
1096            "--override-state",
1097            "0x456:0x2:0x5678",
1098        ]);
1099
1100        assert_eq!(
1101            args.overrides.balance_overrides,
1102            Some(vec!["0x123:0x1234".to_string(), "0x456:0x5678".to_string()])
1103        );
1104        assert_eq!(
1105            args.overrides.nonce_overrides,
1106            Some(vec!["0x123:1".to_string(), "0x456:2".to_string()])
1107        );
1108        assert_eq!(
1109            args.overrides.code_overrides,
1110            Some(vec!["0x123:0x1234".to_string(), "0x456:0x5678".to_string()])
1111        );
1112        assert_eq!(
1113            args.overrides.state_overrides,
1114            Some(vec!["0x123:0x1:0x1234".to_string(), "0x456:0x2:0x5678".to_string()])
1115        );
1116    }
1117
1118    #[test]
1119    fn test_negative_args_with_flags() {
1120        // Test that negative args work with flags
1121        let args = CallArgs::parse_from([
1122            "foundry-cli",
1123            "--trace",
1124            "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
1125            "process(int256)",
1126            "-999999",
1127            "--debug",
1128        ]);
1129
1130        assert!(args.trace);
1131        assert!(args.debug);
1132        assert_eq!(args.args, vec!["-999999"]);
1133    }
1134
1135    #[test]
1136    fn test_transaction_opts_with_trace() {
1137        // Test that transaction options are correctly parsed when using --trace
1138        let args = CallArgs::parse_from([
1139            "foundry-cli",
1140            "--trace",
1141            "--gas-limit",
1142            "1000000",
1143            "--gas-price",
1144            "20000000000",
1145            "--priority-gas-price",
1146            "2000000000",
1147            "--nonce",
1148            "42",
1149            "--value",
1150            "1000000000000000000", // 1 ETH
1151            "--blob-gas-price",
1152            "10000000000",
1153            "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
1154            "balanceOf(address)",
1155            "0x123456789abcdef123456789abcdef123456789a",
1156        ]);
1157
1158        assert!(args.trace);
1159        assert_eq!(args.tx.gas_limit, Some(U256::from(1000000u32)));
1160        assert_eq!(args.tx.gas_price, Some(U256::from(20000000000u64)));
1161        assert_eq!(args.tx.priority_gas_price, Some(U256::from(2000000000u64)));
1162        assert_eq!(args.tx.nonce, Some(U64::from(42)));
1163        assert_eq!(args.tx.value, Some(U256::from(1000000000000000000u64)));
1164        assert_eq!(args.tx.blob_gas_price, Some(U256::from(10000000000u64)));
1165    }
1166
1167    #[test]
1168    fn debug_trace_call_conflicts_with_trace() {
1169        let result = CallArgs::try_parse_from(["foundry-cli", "--trace", "--debug-trace-call"]);
1170        assert!(result.is_err(), "--trace and --debug-trace-call must be mutually exclusive");
1171    }
1172
1173    #[test]
1174    fn debug_trace_call_rejects_local_trace_flags() {
1175        for flag in ["--debug", "--decode-internal"] {
1176            let result = CallArgs::try_parse_from([
1177                "foundry-cli",
1178                "--debug-trace-call",
1179                "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
1180                flag,
1181            ]);
1182            assert!(result.is_err(), "--debug-trace-call must reject {flag}");
1183        }
1184        // --evm-version takes a value, so it is checked separately from the boolean flags above.
1185        let result = CallArgs::try_parse_from([
1186            "foundry-cli",
1187            "--debug-trace-call",
1188            "0xDeaDBeeFcAfEbAbEfAcEfEeDcBaDbEeFcAfEbAbE",
1189            "--evm-version",
1190            "shanghai",
1191        ]);
1192        assert!(result.is_err(), "--debug-trace-call must reject --evm-version");
1193    }
1194
1195    #[test]
1196    fn debug_trace_call_ignores_configured_internal_decoding() {
1197        let args = CallArgs::parse_from(["foundry-cli", "--debug-trace-call"]);
1198        let config = TracingConfig { decode_internal: true, ..Default::default() };
1199
1200        assert!(!args.resolve_tracing(&config, 0).decode_internal);
1201    }
1202}