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