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