Skip to main content

forge_verify/
bytecode.rs

1//! The `forge verify-bytecode` command.
2use crate::{
3    etherscan::EtherscanVerificationProvider,
4    utils::{
5        BytecodeType, JsonResult, check_and_encode_args, check_explorer_args,
6        load_fork_config_and_evm_opts, maybe_predeploy_contract, validate_encoded_constructor_args,
7    },
8    verify::VerifierArgs,
9};
10use alloy_consensus::Transaction as ConsensusTransaction;
11use alloy_network::{AnyNetwork, AnyRpcBlock, AnyRpcTransaction};
12use alloy_primitives::{Address, B256, Bytes, TxKind, U256, hex};
13use alloy_provider::{
14    Provider,
15    ext::TraceApi,
16    network::{BlockResponse, ReceiptResponse, TransactionResponse, primitives::BlockTransactions},
17};
18use alloy_rpc_types::{
19    BlockId, BlockNumberOrTag,
20    trace::parity::{Action, CreateAction, CreateOutput, TraceOutput},
21};
22use clap::{Parser, ValueHint};
23use eyre::{Context, OptionExt, Result};
24use foundry_block_explorers::contract::Metadata;
25use foundry_cli::{
26    opts::EtherscanOpts,
27    utils::{self, LoadConfig, read_constructor_args_file},
28};
29use foundry_common::{
30    SYSTEM_TRANSACTION_TYPE, is_known_system_sender,
31    provider::{ProviderBuilder, RetryProvider},
32    shell,
33};
34use foundry_compilers::info::ContractInfo;
35use foundry_config::{Chain, Config, figment, impl_figment_convert};
36use foundry_evm::{
37    constants::DEFAULT_CREATE2_DEPLOYER,
38    core::{
39        FoundryChain, FoundryTransaction as _,
40        env::FromAnyRpcTransaction as _,
41        evm::{ChainFor, EthEvmNetwork, EvmEnvFor, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor},
42    },
43    executors::{EvmError, Executor, ExecutorBuilder, TracingExecutor},
44    opts::{EvmOpts, ForkEndpointIdentity},
45    utils::apply_chain_specific_tx_replay_env_changes_for_chain,
46};
47use foundry_evm_networks::NetworkVariant;
48use revm::{context::Block as _, state::AccountInfo};
49use std::path::PathBuf;
50
51#[cfg(feature = "base")]
52use foundry_evm::core::evm::BaseEvmNetwork;
53
54#[cfg(feature = "monad")]
55use foundry_evm::core::evm::{BlockContext, MonadEvmNetwork};
56
57#[cfg(feature = "optimism")]
58use foundry_evm::core::evm::OpEvmNetwork;
59
60impl_figment_convert!(VerifyBytecodeArgs);
61
62/// CLI arguments for `forge verify-bytecode`.
63#[derive(Clone, Debug, Parser)]
64pub struct VerifyBytecodeArgs {
65    /// The address of the contract to verify.
66    pub address: Address,
67
68    /// The contract identifier in the form `<path>:<contractname>`.
69    pub contract: ContractInfo,
70
71    /// The block at which the bytecode should be verified.
72    #[arg(long, value_name = "BLOCK")]
73    pub block: Option<BlockId>,
74
75    /// The constructor args to generate the creation code.
76    #[arg(
77        long,
78        num_args(1..),
79        conflicts_with_all = &["constructor_args_path", "encoded_constructor_args"],
80        value_name = "ARGS",
81    )]
82    pub constructor_args: Option<Vec<String>>,
83
84    /// The ABI-encoded constructor arguments.
85    #[arg(
86        long,
87        conflicts_with_all = &["constructor_args_path", "constructor_args"],
88        value_name = "HEX",
89    )]
90    pub encoded_constructor_args: Option<String>,
91
92    /// The path to a file containing the constructor arguments.
93    #[arg(
94        long,
95        value_hint = ValueHint::FilePath,
96        value_name = "PATH",
97        conflicts_with_all = &["constructor_args", "encoded_constructor_args"]
98    )]
99    pub constructor_args_path: Option<PathBuf>,
100
101    /// The rpc url to use for verification.
102    #[arg(short = 'r', long, value_name = "RPC_URL", env = "ETH_RPC_URL")]
103    pub rpc_url: Option<String>,
104
105    /// Specify the network for correct encoding.
106    #[arg(long, short, num_args = 1, value_name = "NETWORK")]
107    pub network: Option<NetworkVariant>,
108
109    /// Etherscan options.
110    #[command(flatten)]
111    pub etherscan: EtherscanOpts,
112
113    /// Verifier options.
114    #[command(flatten)]
115    pub verifier: VerifierArgs,
116
117    /// Set pre-linked libraries.
118    #[arg(long, help_heading = "Linker options")]
119    pub libraries: Vec<String>,
120
121    /// The project's root path.
122    ///
123    /// By default root of the Git repository, if in one,
124    /// or the current working directory.
125    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
126    pub root: Option<PathBuf>,
127
128    /// Ignore verification for creation or runtime bytecode.
129    #[arg(long, value_name = "BYTECODE_TYPE")]
130    pub ignore: Option<BytecodeType>,
131}
132
133impl figment::Provider for VerifyBytecodeArgs {
134    fn metadata(&self) -> figment::Metadata {
135        figment::Metadata::named("Verify Bytecode Provider")
136    }
137
138    fn data(
139        &self,
140    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
141        let mut dict = self.etherscan.dict();
142
143        if let Some(api_key) = &self.verifier.verifier_api_key {
144            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
145        }
146
147        if let Some(block) = &self.block {
148            dict.insert("block".into(), figment::value::Value::serialize(block)?);
149        }
150        if let Some(rpc_url) = &self.rpc_url {
151            dict.insert("eth_rpc_url".into(), rpc_url.clone().into());
152        }
153
154        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
155    }
156}
157
158impl VerifyBytecodeArgs {
159    fn configured_network(
160        cli_network: Option<NetworkVariant>,
161        config: &Config,
162    ) -> Option<NetworkVariant> {
163        cli_network.or_else(|| {
164            config.networks.has_network_selection().then(|| config.networks.execution_network())
165        })
166    }
167
168    async fn endpoint_identity(config: &Config) -> Result<Option<ForkEndpointIdentity>> {
169        let (_, mut evm_opts) = load_fork_config_and_evm_opts(config)?;
170        if evm_opts.fork_url.is_none() {
171            evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned());
172        }
173        evm_opts.infer_network_from_fork().await?;
174        Ok(evm_opts.fork_endpoint)
175    }
176
177    async fn ensure_endpoint_identity_unchanged(
178        config: &Config,
179        expected: Option<&ForkEndpointIdentity>,
180    ) -> Result<()> {
181        let Some(expected) = expected else { return Ok(()) };
182        let current = Self::endpoint_identity(config).await?.ok_or_else(|| {
183            eyre::eyre!("RPC endpoint identity disappeared while verify-bytecode was running")
184        })?;
185        Self::validate_endpoint_identity(expected, &current)
186    }
187
188    fn validate_endpoint_identity(
189        expected: &ForkEndpointIdentity,
190        current: &ForkEndpointIdentity,
191    ) -> Result<()> {
192        if current != expected {
193            eyre::bail!(
194                "RPC endpoint identity changed while verify-bytecode was running; retry against \
195                 a stable endpoint"
196            );
197        }
198        Ok(())
199    }
200
201    fn apply_endpoint_expectation(
202        evm_opts: &mut EvmOpts,
203        endpoint_identity: Option<&ForkEndpointIdentity>,
204        network_was_inferred: bool,
205    ) {
206        if let Some(identity) = endpoint_identity {
207            evm_opts.expect_fork_endpoint(identity.clone(), network_was_inferred);
208        }
209    }
210
211    fn effective_network(
212        configured: Option<NetworkVariant>,
213        endpoint_identity: Option<&ForkEndpointIdentity>,
214    ) -> NetworkVariant {
215        configured
216            .or_else(|| endpoint_identity.map(|identity| identity.network))
217            .unwrap_or(NetworkVariant::Ethereum)
218    }
219
220    fn materialize_execution_network(
221        config: &mut Config,
222        endpoint_identity: Option<&ForkEndpointIdentity>,
223    ) -> NetworkVariant {
224        let configured = Self::configured_network(None, config);
225        let network = Self::effective_network(configured, endpoint_identity);
226        if configured.is_none() {
227            config.networks = if let Some(identity) = endpoint_identity {
228                config.networks.with_rpc_profile(identity.network_profile)
229            } else {
230                network.into()
231            };
232        }
233        network
234    }
235
236    fn explorer_chain(
237        configured: Option<Chain>,
238        endpoint_identity: Option<&ForkEndpointIdentity>,
239    ) -> Option<Chain> {
240        configured.or_else(|| endpoint_identity.map(|identity| identity.source_chain_id.into()))
241    }
242
243    /// Run the `verify-bytecode` command to verify the bytecode onchain against the locally built
244    /// bytecode.
245    pub async fn run(mut self) -> Result<()> {
246        let mut config = self.load_config_with_dependencies()?;
247        config.libraries.append(&mut self.libraries);
248
249        if let Some(network) = self.network {
250            config.networks = network.into();
251        }
252        let network_was_inferred = Self::configured_network(None, &config).is_none();
253        let endpoint_identity = Self::endpoint_identity(&config).await?;
254        let network = Self::materialize_execution_network(&mut config, endpoint_identity.as_ref());
255
256        match network {
257            NetworkVariant::Ethereum => {
258                self.run_with_network::<EthEvmNetwork>(
259                    config,
260                    endpoint_identity,
261                    network_was_inferred,
262                    ExecutorBuilder::<EthEvmNetwork>::new(),
263                )
264                .await
265            }
266            #[cfg(feature = "base")]
267            NetworkVariant::Base => {
268                self.run_with_network::<BaseEvmNetwork>(
269                    config,
270                    endpoint_identity,
271                    network_was_inferred,
272                    ExecutorBuilder::<BaseEvmNetwork>::new(),
273                )
274                .await
275            }
276            #[cfg(feature = "optimism")]
277            NetworkVariant::Optimism => {
278                self.run_with_network::<OpEvmNetwork>(
279                    config,
280                    endpoint_identity,
281                    network_was_inferred,
282                    ExecutorBuilder::<OpEvmNetwork>::new(),
283                )
284                .await
285            }
286            NetworkVariant::Tempo => {
287                self.run_with_network::<TempoEvmNetwork>(
288                    config,
289                    endpoint_identity,
290                    network_was_inferred,
291                    ExecutorBuilder::<TempoEvmNetwork>::new(),
292                )
293                .await
294            }
295            #[cfg(feature = "monad")]
296            NetworkVariant::Monad => {
297                let Some(mut verification) = self
298                    .prepare_runtime_verification::<MonadEvmNetwork>(
299                        config,
300                        endpoint_identity,
301                        network_was_inferred,
302                        ExecutorBuilder::<MonadEvmNetwork>::new(),
303                    )
304                    .await?
305                else {
306                    return Ok(());
307                };
308                let context = replay_monad_block_transactions(
309                    &verification.config,
310                    verification.block.as_ref(),
311                    verification.simulation_block,
312                    verification.transaction.tx_hash(),
313                    &mut verification.executor,
314                    &verification.evm_env,
315                )
316                .await?;
317                verification.finish(context).await
318            }
319        }
320    }
321
322    /// Runs verification for networks whose replay does not require block ancestry.
323    async fn run_with_network<FEN: FoundryEvmNetwork>(
324        self,
325        config: Config,
326        endpoint_identity: Option<ForkEndpointIdentity>,
327        network_was_inferred: bool,
328        executor_builder: ExecutorBuilder<FEN>,
329    ) -> Result<()> {
330        let Some(mut verification) = self
331            .prepare_runtime_verification::<FEN>(
332                config,
333                endpoint_identity,
334                network_was_inferred,
335                executor_builder,
336            )
337            .await?
338        else {
339            return Ok(());
340        };
341        let context = replay_block_transactions(
342            verification.block.as_ref(),
343            verification.transaction.tx_hash(),
344            &mut verification.executor,
345            &verification.evm_env,
346        )?;
347        verification.finish(context).await
348    }
349
350    async fn prepare_runtime_verification<FEN>(
351        mut self,
352        config: Config,
353        endpoint_identity: Option<ForkEndpointIdentity>,
354        network_was_inferred: bool,
355        executor_builder: ExecutorBuilder<FEN>,
356    ) -> Result<Option<RuntimeVerification<FEN>>>
357    where
358        FEN: FoundryEvmNetwork,
359    {
360        // Setup
361        // `AnyNetwork` rather than `FEN::Network`: chains such as Arbitrum and Celo put
362        // transaction types the strict Ethereum envelope cannot decode into every block, which
363        // would fail the full block fetches below for the whole chain. Execution still uses `FEN`.
364        let provider = ProviderBuilder::<AnyNetwork>::from_config(&config)?.build()?;
365
366        // If chain is not set, we try to get it from the RPC.
367        // If RPC is not set, the default chain is used.
368        let chain = match (
369            Self::explorer_chain(config.chain, endpoint_identity.as_ref()),
370            config.get_rpc_url(),
371        ) {
372            (Some(chain), _) => chain,
373            (None, Some(_)) => utils::get_chain::<AnyNetwork, _>(None, &provider).await?,
374            (None, None) => Default::default(),
375        };
376
377        // Set Etherscan options.
378        self.etherscan.chain = Some(chain);
379        self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
380
381        // Whether a block explorer is configured for this chain. Client setup errors are only
382        // treated as "no explorer available" when no usable verifier, verifier URL, or resolved
383        // API key is configured.
384        let has_explorer_config = self.verifier.verifier.is_some()
385            || self.verifier.verifier_url.is_some()
386            || self.verifier.verifier_api_key.is_some()
387            || self.etherscan.key.is_some();
388
389        // Etherscan client. May be unavailable (e.g. unknown chain, missing configuration), in
390        // which case verification proceeds with local data only.
391        let etherscan = match EtherscanVerificationProvider.client(
392            &self.etherscan,
393            &self.verifier,
394            &config,
395        ) {
396            Ok(client) => Some(client),
397            Err(err) => {
398                if has_explorer_config {
399                    return Err(err);
400                }
401                if !shell::is_json() {
402                    sh_warn!(
403                        "Failed to create a block explorer client: {err}. Continuing with the local project configuration."
404                    )?;
405                }
406                None
407            }
408        };
409
410        // Get the bytecode at the address, bailing if it doesn't exist.
411        let code = provider.get_code_at(self.address).await?;
412        Self::ensure_endpoint_identity_unchanged(&config, endpoint_identity.as_ref()).await?;
413        if code.is_empty() {
414            eyre::bail!("No bytecode found at address {}", self.address);
415        }
416
417        if !shell::is_json() {
418            sh_status!(
419                "Verifying bytecode for contract {} at address {}",
420                self.contract.name,
421                self.address
422            )?;
423        }
424
425        let mut json_results: Vec<JsonResult> = vec![];
426
427        // Get creation tx hash. An unavailable explorer (missing API key, unsupported chain,
428        // unverified contract, etc.) must not prevent verification against a local build: fall
429        // back to verifying the runtime bytecode only.
430        // See <https://github.com/foundry-rs/foundry/issues/13479>.
431        let (creation_data, maybe_predeploy) = match &etherscan {
432            Some(etherscan) => {
433                let creation_data = etherscan.contract_creation_data(self.address).await;
434
435                // Check if contract is a predeploy
436                match maybe_predeploy_contract(creation_data) {
437                    Ok(res) => res,
438                    Err(err) => {
439                        if has_explorer_config {
440                            return Err(err);
441                        }
442                        if !shell::is_json() {
443                            sh_warn!(
444                                "Failed to fetch creation data from the block explorer: {err}"
445                            )?;
446                        }
447                        (None, false)
448                    }
449                }
450            }
451            None => (None, false),
452        };
453
454        trace!(maybe_predeploy = ?maybe_predeploy);
455
456        // Get the constructor args using `source_code` endpoint.
457        let source_code = match &etherscan {
458            Some(etherscan) => match etherscan.contract_source_code(self.address).await {
459                Ok(source_code) => {
460                    if let Some(metadata) = source_code.items.first() {
461                        // Check if the contract name matches.
462                        if metadata.contract_name != self.contract.name {
463                            eyre::bail!("Contract name mismatch");
464                        }
465                        Some(source_code)
466                    } else {
467                        if !shell::is_json() {
468                            sh_warn!(
469                                "Block explorer returned no source metadata. Continuing with the local project configuration; compiler settings mismatches will not be reported."
470                            )?;
471                        }
472                        None
473                    }
474                }
475                Err(err) => {
476                    if has_explorer_config {
477                        return Err(err.into());
478                    }
479                    if !shell::is_json() {
480                        sh_warn!(
481                            "Failed to fetch contract source code from the block explorer: {err}. Continuing with the local project configuration; compiler settings mismatches will not be reported."
482                        )?;
483                    }
484                    None
485                }
486            },
487            None => None,
488        };
489
490        // Obtain Etherscan compilation metadata.
491        let etherscan_metadata = source_code.as_ref().and_then(|source| source.items.first());
492
493        // Obtain local artifact
494        let artifact = crate::utils::build_project(&self, &config)?;
495
496        // Get local bytecode (creation code)
497        let local_bytecode = artifact
498            .bytecode
499            .as_ref()
500            .and_then(|b| b.to_owned().into_bytes())
501            .ok_or_eyre("Unlinked bytecode is not supported for verification")?;
502
503        // Get and encode user provided constructor args
504        let provided_constructor_args = if let Some(encoded) = &self.encoded_constructor_args {
505            Some(validate_encoded_constructor_args(&artifact, hex::decode(encoded)?)?)
506        } else {
507            if let Some(path) = self.constructor_args_path.clone() {
508                // Read from file.
509                Some(read_constructor_args_file(path)?)
510            } else {
511                self.constructor_args.clone()
512            }
513            .map(|args| check_and_encode_args(&artifact, args))
514            .transpose()?
515        };
516
517        let args_from_user = provided_constructor_args.is_some();
518        let mut constructor_args = if let Some(provided) = provided_constructor_args {
519            provided.into()
520        } else if let Some(source_code) = &source_code {
521            // If no constructor args were provided, try to retrieve them from the explorer.
522            check_explorer_args(source_code)?
523        } else {
524            Bytes::new()
525        };
526
527        // This fails only when the contract expects constructor args but NONE were provided OR
528        // retrieved from explorer (in case of predeploys).
529        crate::utils::check_args_len(&artifact, &constructor_args)?;
530
531        // Without creation data (predeploys, or the explorer being unavailable), the creation
532        // code cannot be verified. Verify the runtime bytecode instead by deploying the local
533        // creation code and comparing the resulting runtime code with the onchain one.
534        if creation_data.is_none() {
535            if !shell::is_json() {
536                if maybe_predeploy {
537                    sh_warn!(
538                        "Attempting to verify predeployed contract at {:?}. Ignoring creation code verification.",
539                        self.address
540                    )?;
541                } else {
542                    sh_warn!("Creation data is unavailable. Ignoring creation code verification.")?;
543                }
544            }
545
546            // Without creation data there is nothing else to verify when the runtime bytecode is
547            // ignored.
548            if self.ignore.is_some_and(|b| b.is_runtime()) {
549                if shell::is_json() {
550                    sh_println!("{}", serde_json::to_string(&json_results)?)?;
551                }
552                return Ok(None);
553            }
554
555            let deploy_block = if maybe_predeploy {
556                // Deploy at genesis
557                0_u64
558            } else {
559                match self.block {
560                    Some(BlockId::Number(BlockNumberOrTag::Number(block))) => block,
561                    Some(_) => {
562                        eyre::bail!("Invalid block number");
563                    }
564                    None => provider.get_block_number().await?,
565                }
566            };
567
568            // Append constructor args to the local_bytecode.
569            trace!(%constructor_args);
570            let mut local_bytecode_vec = local_bytecode.to_vec();
571            local_bytecode_vec.extend_from_slice(&constructor_args);
572
573            let deploy_block_info = provider.get_block(deploy_block.into()).full().await?;
574            let (mut fork_config, mut evm_opts) = load_fork_config_and_evm_opts(&config)?;
575            Self::apply_endpoint_expectation(
576                &mut evm_opts,
577                endpoint_identity.as_ref(),
578                network_was_inferred,
579            );
580            let (evm_env, _, mut executor) = crate::utils::get_tracing_executor::<FEN>(
581                &mut fork_config,
582                deploy_block,
583                deploy_block,
584                deploy_block_info.as_ref(),
585                evm_opts,
586                executor_builder.clone(),
587            )
588            .await?;
589            Self::ensure_endpoint_identity_unchanged(&config, endpoint_identity.as_ref()).await?;
590
591            // Setup genesis tx_env and evm_evm.
592            let deployer = Address::with_last_byte(0x1);
593            let mut tx_env = TxEnvFor::<FEN>::default();
594            tx_env.set_caller(deployer);
595            tx_env.set_kind(TxKind::Create);
596            tx_env.set_data(Bytes::from(local_bytecode_vec));
597            tx_env.set_chain_id(Some(evm_env.cfg_env.chain_id));
598            tx_env.set_gas_limit(evm_env.block_env.gas_limit());
599            tx_env.set_gas_price(evm_env.block_env.basefee() as u128);
600
601            // Seed deployer account with funds
602            let account_info = AccountInfo {
603                balance: U256::from(100 * 10_u128.pow(18)),
604                nonce: 0,
605                ..Default::default()
606            };
607            executor.backend_mut().insert_account_info(deployer, account_info);
608
609            let fork_address = if maybe_predeploy || deploy_block == 0 {
610                crate::utils::deploy_contract::<FEN>(
611                    &mut executor,
612                    &evm_env,
613                    &tx_env,
614                    TxKind::Create,
615                    ChainFor::<FEN>::for_transaction(&tx_env),
616                )?
617            } else {
618                executor.deploy_with_env(evm_env.clone(), tx_env.clone(), None)?.address
619            };
620
621            // Compare runtime bytecode. The onchain code is read at `deploy_block` to stay
622            // anchored to the same height as the local fork. Predeploys keep reading at the
623            // latest block: their code is stable and genesis state often isn't served by RPCs.
624            let (deployed_bytecode, onchain_runtime_code) = crate::utils::get_runtime_codes::<FEN>(
625                &mut executor,
626                &provider,
627                self.address,
628                fork_address,
629                (!maybe_predeploy).then_some(deploy_block),
630            )
631            .await?;
632            Self::ensure_endpoint_identity_unchanged(&config, endpoint_identity.as_ref()).await?;
633
634            let match_type = crate::utils::match_bytecodes(
635                deployed_bytecode.original_byte_slice(),
636                &onchain_runtime_code,
637                &constructor_args,
638                true,
639                config.bytecode_hash,
640            );
641
642            crate::utils::print_result(
643                match_type,
644                BytecodeType::Runtime,
645                &mut json_results,
646                etherscan_metadata,
647                &config,
648            );
649
650            if shell::is_json() {
651                sh_println!("{}", serde_json::to_string(&json_results)?)?;
652            }
653
654            return Ok(None);
655        }
656
657        // We can unwrap directly as maybe_predeploy is false
658        let creation_data = creation_data.unwrap();
659        // Get transaction and receipt.
660        trace!(creation_tx_hash = ?creation_data.transaction_hash);
661        let transaction = provider
662            .get_transaction_by_hash(creation_data.transaction_hash)
663            .await
664            .or_else(|e| {
665                eyre::bail!("Couldn't fetch transaction from RPC: {:?}", e);
666            })?
667            .ok_or_else(|| {
668                eyre::eyre!("Transaction not found for hash {}", creation_data.transaction_hash)
669            })?;
670        let receipt = provider
671            .get_transaction_receipt(creation_data.transaction_hash)
672            .await
673            .or_else(|e| {
674                eyre::bail!("Couldn't fetch transaction receipt from RPC: {:?}", e);
675            })?;
676        let receipt = if let Some(receipt) = receipt {
677            receipt
678        } else {
679            eyre::bail!(
680                "Receipt not found for transaction hash {}",
681                creation_data.transaction_hash
682            );
683        };
684
685        let creation_block = transaction.block_number();
686
687        // Extract creation code from creation tx input.
688        let maybe_creation_code = if receipt.to().is_none()
689            && receipt.contract_address() == Some(self.address)
690        {
691            transaction.input().clone()
692        } else if receipt.to() == Some(DEFAULT_CREATE2_DEPLOYER) {
693            Bytes::copy_from_slice(&transaction.input()[32..])
694        } else {
695            // Try to get creation bytecode from tx trace.
696            let traces = provider
697                .trace_transaction(creation_data.transaction_hash)
698                .await
699                .unwrap_or_default();
700
701            let creation_bytecode =
702                traces.iter().find_map(|trace| match (&trace.trace.result, &trace.trace.action) {
703                    (
704                        Some(TraceOutput::Create(CreateOutput { address, .. })),
705                        Action::Create(CreateAction { init, .. }),
706                    ) if *address == self.address => Some(init.clone()),
707                    _ => None,
708                });
709
710            creation_bytecode.ok_or_else(|| {
711                eyre::eyre!(
712                    "Could not extract the creation code for contract at address {}",
713                    self.address
714                )
715            })?
716        };
717        Self::ensure_endpoint_identity_unchanged(&config, endpoint_identity.as_ref()).await?;
718
719        // In some cases, Etherscan will return incorrect constructor arguments. If this
720        // happens, try extracting arguments ourselves. Never replace user-provided arguments.
721        if !args_from_user && !maybe_creation_code.ends_with(&constructor_args) {
722            trace!("mismatch of constructor args with etherscan");
723            if maybe_creation_code.len() >= local_bytecode.len() {
724                // If local bytecode is longer than on-chain one, this is probably not a match.
725                constructor_args =
726                    Bytes::copy_from_slice(&maybe_creation_code[local_bytecode.len()..]);
727                trace!(
728                    target: "forge::verify",
729                    "setting constructor args to latest {} bytes of bytecode",
730                    constructor_args.len()
731                );
732            }
733        }
734
735        // Append constructor args to the local_bytecode.
736        trace!(%constructor_args);
737        let mut local_bytecode_vec = local_bytecode.to_vec();
738        local_bytecode_vec.extend_from_slice(&constructor_args);
739
740        // A suffix check alone is insufficient for dynamic ABI values: one valid encoding can
741        // be a suffix of a different valid encoding. Always compare the complete creation code
742        // when arguments came from the user, even if creation output is ignored.
743        let creation_match_type = crate::utils::match_bytecodes(
744            local_bytecode_vec.as_slice(),
745            &maybe_creation_code,
746            &constructor_args,
747            false,
748            config.bytecode_hash,
749        );
750        if args_from_user
751            && creation_match_type.is_none()
752            && self.ignore.is_none_or(|b| !b.is_creation())
753        {
754            let message =
755                "Provided constructor args could not be validated against deployment creation code";
756            if shell::is_json() {
757                json_results.push(JsonResult {
758                    bytecode_type: BytecodeType::Creation,
759                    match_type: None,
760                    message: Some(message.to_string()),
761                });
762                if self.ignore.is_none_or(|b| !b.is_runtime()) {
763                    json_results.push(JsonResult {
764                        bytecode_type: BytecodeType::Runtime,
765                        match_type: None,
766                        message: Some(message.to_string()),
767                    });
768                }
769                sh_println!("{}", serde_json::to_string(&json_results)?)?;
770            } else {
771                sh_warn!("{message}")?;
772                crate::utils::print_result(
773                    None,
774                    BytecodeType::Creation,
775                    &mut json_results,
776                    etherscan_metadata,
777                    &config,
778                );
779                if self.ignore.is_none_or(|b| !b.is_runtime()) {
780                    crate::utils::print_result(
781                        None,
782                        BytecodeType::Runtime,
783                        &mut json_results,
784                        etherscan_metadata,
785                        &config,
786                    );
787                }
788            }
789            return Ok(None);
790        }
791
792        trace!(ignore = ?self.ignore);
793        // Check if `--ignore` is set to `creation`.
794        if self.ignore.is_none_or(|b| !b.is_creation()) {
795            // Compare creation code with locally built bytecode and `maybe_creation_code`.
796            crate::utils::print_result(
797                creation_match_type,
798                BytecodeType::Creation,
799                &mut json_results,
800                etherscan_metadata,
801                &config,
802            );
803
804            // If the creation code does not match, the runtime also won't match. Hence return.
805            if creation_match_type.is_none() {
806                crate::utils::print_result(
807                    None,
808                    BytecodeType::Runtime,
809                    &mut json_results,
810                    etherscan_metadata,
811                    &config,
812                );
813                if shell::is_json() {
814                    sh_println!("{}", serde_json::to_string(&json_results)?)?;
815                }
816                return Ok(None);
817            }
818        }
819
820        if self.ignore.is_none_or(|b| !b.is_runtime()) {
821            // Runtime verification can only re-deploy local bytecode for direct `CREATE` and the
822            // default `CREATE2` deployer, so skip custom factory deployments.
823            if let TxKind::Call(to) = ConsensusTransaction::kind(&transaction)
824                && to != DEFAULT_CREATE2_DEPLOYER
825            {
826                let message = format!(
827                    "Runtime bytecode verification is not supported for this contract: its \
828                     creation transaction calls custom factory {to}. forge can only verify \
829                     runtime bytecode for direct CREATE transactions and calls to the default \
830                     CREATE2 deployer; skipping runtime bytecode verification."
831                );
832                if shell::is_json() {
833                    json_results.push(JsonResult {
834                        bytecode_type: BytecodeType::Runtime,
835                        match_type: None,
836                        message: Some(message),
837                    });
838                    sh_println!("{}", serde_json::to_string(&json_results)?)?;
839                } else {
840                    sh_warn!("{message}")?;
841                }
842                return Ok(None);
843            }
844
845            // Get contract creation block.
846            let simulation_block = match self.block {
847                Some(BlockId::Number(BlockNumberOrTag::Number(block))) => block,
848                Some(_) => {
849                    eyre::bail!("Invalid block number");
850                }
851                None => creation_block.ok_or_else(|| {
852                    eyre::eyre!(
853                        "Failed to get block number of the contract creation tx, specify using the \
854                         --block flag"
855                    )
856                })?,
857            };
858
859            // Fork the chain immediately before `simulation_block`, then execute with the target
860            // block's environment and effective runtime hardfork.
861            let block = provider.get_block(simulation_block.into()).full().await?;
862            let (mut fork_config, mut evm_opts) = load_fork_config_and_evm_opts(&config)?;
863            Self::apply_endpoint_expectation(
864                &mut evm_opts,
865                endpoint_identity.as_ref(),
866                network_was_inferred,
867            );
868            let (mut evm_env, _tx_env, executor) = crate::utils::get_tracing_executor::<FEN>(
869                &mut fork_config,
870                simulation_block - 1, // env.fork_block_number
871                simulation_block,
872                block.as_ref(),
873                evm_opts,
874                executor_builder,
875            )
876            .await?;
877            Self::ensure_endpoint_identity_unchanged(&config, endpoint_identity.as_ref()).await?;
878
879            // Workaround for the NonceTooHigh issue as we're not simulating prior txs of the same
880            // block.
881            let prev_block_id = BlockId::number(simulation_block - 1);
882
883            // Use `transaction.from` instead of `creation_data.contract_creator` to resolve
884            // blockscout creation data discrepancy in case of CREATE2.
885            let prev_block_nonce =
886                provider.get_transaction_count(transaction.from()).block_id(prev_block_id).await?;
887
888            apply_chain_specific_tx_replay_env_changes_for_chain(&mut evm_env, chain.id());
889            return Ok(Some(RuntimeVerification {
890                address: self.address,
891                config,
892                endpoint_identity,
893                provider,
894                executor,
895                evm_env,
896                block,
897                simulation_block,
898                transaction,
899                prev_block_nonce,
900                local_bytecode_vec,
901                constructor_args,
902                json_results,
903                etherscan_metadata: source_code.and_then(|source| source.items.into_iter().next()),
904            }));
905        }
906        if shell::is_json() {
907            sh_println!("{}", serde_json::to_string(&json_results)?)?;
908        }
909        Ok(None)
910    }
911}
912
913/// Prepared runtime verification, before replaying the creation block's prefix.
914struct RuntimeVerification<FEN: FoundryEvmNetwork> {
915    address: Address,
916    config: Config,
917    endpoint_identity: Option<ForkEndpointIdentity>,
918    provider: RetryProvider,
919    executor: TracingExecutor<FEN>,
920    evm_env: EvmEnvFor<FEN>,
921    block: Option<AnyRpcBlock>,
922    simulation_block: u64,
923    transaction: AnyRpcTransaction,
924    prev_block_nonce: u64,
925    local_bytecode_vec: Vec<u8>,
926    constructor_args: Bytes,
927    json_results: Vec<JsonResult>,
928    etherscan_metadata: Option<Metadata>,
929}
930
931impl<FEN: FoundryEvmNetwork> RuntimeVerification<FEN> {
932    async fn finish(self, target_context: Option<ChainFor<FEN>>) -> Result<()> {
933        let Self {
934            address,
935            config,
936            endpoint_identity,
937            provider,
938            mut executor,
939            evm_env,
940            simulation_block,
941            transaction,
942            prev_block_nonce,
943            local_bytecode_vec,
944            constructor_args,
945            mut json_results,
946            etherscan_metadata,
947            ..
948        } = self;
949        let kind = ConsensusTransaction::kind(&transaction);
950        let mut tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(&transaction)?;
951        tx_env.set_nonce(prev_block_nonce);
952        let target_context =
953            target_context.unwrap_or_else(|| ChainFor::<FEN>::for_transaction(&tx_env));
954
955        // Replace the `input` with local creation code in the creation tx.
956        if let TxKind::Call(to) = kind {
957            if to == DEFAULT_CREATE2_DEPLOYER {
958                let mut input = transaction.input()[..32].to_vec(); // Salt
959                input.extend_from_slice(&local_bytecode_vec);
960                tx_env.set_data(Bytes::from(input));
961
962                // Deploy default CREATE2 deployer
963                executor.deploy_create2_deployer()?;
964            }
965        } else {
966            tx_env.set_data(Bytes::from(local_bytecode_vec));
967        }
968
969        let fork_address = crate::utils::deploy_contract::<FEN>(
970            &mut executor,
971            &evm_env,
972            &tx_env,
973            kind,
974            target_context,
975        )?;
976
977        // State committed using deploy_with_env, now get the runtime bytecode from the db.
978        let (fork_runtime_code, onchain_runtime_code) = crate::utils::get_runtime_codes::<FEN>(
979            &mut executor,
980            &provider,
981            address,
982            fork_address,
983            Some(simulation_block),
984        )
985        .await?;
986        VerifyBytecodeArgs::ensure_endpoint_identity_unchanged(&config, endpoint_identity.as_ref())
987            .await?;
988
989        // Compare the onchain runtime bytecode with the runtime code from the fork.
990        let match_type = crate::utils::match_bytecodes(
991            fork_runtime_code.original_byte_slice(),
992            &onchain_runtime_code,
993            &constructor_args,
994            true,
995            config.bytecode_hash,
996        );
997
998        crate::utils::print_result(
999            match_type,
1000            BytecodeType::Runtime,
1001            &mut json_results,
1002            etherscan_metadata.as_ref(),
1003            &config,
1004        );
1005        if shell::is_json() {
1006            sh_println!("{}", serde_json::to_string(&json_results)?)?;
1007        }
1008        Ok(())
1009    }
1010}
1011
1012/// Replays ordinary transactions preceding `target_hash` and returns its execution context.
1013fn replay_block_transactions<FEN: FoundryEvmNetwork>(
1014    block: Option<&AnyRpcBlock>,
1015    target_hash: B256,
1016    executor: &mut Executor<FEN>,
1017    evm_env: &EvmEnvFor<FEN>,
1018) -> Result<Option<ChainFor<FEN>>> {
1019    let Some(block) = block else { return Ok(None) };
1020    let BlockTransactions::Full(txs) = block.transactions() else {
1021        return Err(eyre::eyre!("Could not get block txs"));
1022    };
1023    let target_tx = txs
1024        .iter()
1025        .find(|tx| tx.tx_hash() == target_hash)
1026        .ok_or_else(|| eyre::eyre!("transaction {target_hash:?} is missing from its block"))?;
1027    let target_tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(target_tx)?;
1028
1029    for tx in txs {
1030        trace!("replay tx::: {}", tx.tx_hash());
1031        if tx.tx_hash() == target_hash {
1032            break;
1033        }
1034        if is_known_system_sender(tx.from())
1035            || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE)
1036        {
1037            continue;
1038        }
1039
1040        let tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(tx)?;
1041        let chain_context = ChainFor::<FEN>::for_transaction(&tx_env);
1042        execute_replay_transaction(executor, evm_env, tx, tx_env, chain_context)?;
1043    }
1044
1045    Ok(Some(ChainFor::<FEN>::for_transaction(&target_tx_env)))
1046}
1047
1048/// Replays Monad transactions preceding `target_hash` with their ancestry context.
1049#[cfg(feature = "monad")]
1050async fn replay_monad_block_transactions(
1051    config: &Config,
1052    block: Option<&AnyRpcBlock>,
1053    block_number: u64,
1054    target_hash: B256,
1055    executor: &mut Executor<MonadEvmNetwork>,
1056    evm_env: &EvmEnvFor<MonadEvmNetwork>,
1057) -> Result<Option<ChainFor<MonadEvmNetwork>>> {
1058    let block = block.ok_or_else(|| {
1059        eyre::eyre!("block {block_number} is required to reconstruct transaction context")
1060    })?;
1061    let BlockTransactions::Full(txs) = block.transactions() else {
1062        return Err(eyre::eyre!("Could not get block txs"));
1063    };
1064    let block_context = monad_block_context(config, block_number).await?;
1065    let target_index = txs
1066        .iter()
1067        .position(|tx| tx.tx_hash() == target_hash)
1068        .ok_or_else(|| eyre::eyre!("transaction {target_hash:?} is missing from its block"))?;
1069
1070    for (index, tx) in txs.iter().enumerate() {
1071        trace!("replay tx::: {}", tx.tx_hash());
1072        if tx.tx_hash() == target_hash {
1073            break;
1074        }
1075
1076        let tx_env = TxEnvFor::<MonadEvmNetwork>::from_any_rpc_transaction(tx)?;
1077        let chain_context = block_context.transaction(index);
1078        if is_known_system_sender(tx.from())
1079            || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE)
1080        {
1081            let _ = executor
1082                .try_transact_system_replay_with_env_and_context(
1083                    evm_env.clone(),
1084                    tx_env,
1085                    chain_context,
1086                )
1087                .wrap_err_with(|| {
1088                    format!(
1089                        "Failed to replay system transaction: {:?} in block {}",
1090                        tx.tx_hash(),
1091                        evm_env.block_env.number()
1092                    )
1093                })?;
1094            continue;
1095        }
1096
1097        execute_replay_transaction(executor, evm_env, tx, tx_env, chain_context)?;
1098    }
1099
1100    Ok(Some(block_context.transaction(target_index)))
1101}
1102
1103fn execute_replay_transaction<FEN: FoundryEvmNetwork>(
1104    executor: &mut Executor<FEN>,
1105    evm_env: &EvmEnvFor<FEN>,
1106    tx: &alloy_network::AnyRpcTransaction,
1107    tx_env: TxEnvFor<FEN>,
1108    chain_context: ChainFor<FEN>,
1109) -> Result<()> {
1110    if ConsensusTransaction::to(tx).is_some() {
1111        executor
1112            .transact_with_env_and_context(evm_env.clone(), tx_env, chain_context)
1113            .wrap_err_with(|| {
1114                format!(
1115                    "Failed to execute transaction: {:?} in block {}",
1116                    tx.tx_hash(),
1117                    evm_env.block_env.number()
1118                )
1119            })?;
1120    } else if let Err(error) =
1121        executor.deploy_with_env_and_context(evm_env.clone(), tx_env, chain_context, None)
1122    {
1123        match error {
1124            // Reverted transactions should be skipped.
1125            EvmError::Execution(_) => (),
1126            error => {
1127                return Err(error).wrap_err_with(|| {
1128                    format!(
1129                        "Failed to deploy transaction: {:?} in block {}",
1130                        tx.tx_hash(),
1131                        evm_env.block_env.number()
1132                    )
1133                });
1134            }
1135        }
1136    }
1137    Ok(())
1138}
1139
1140/// Fetches the block context Monad needs to reconstruct replay ordering.
1141#[cfg(feature = "monad")]
1142async fn monad_block_context(
1143    config: &Config,
1144    block_number: u64,
1145) -> Result<BlockContext<MonadEvmNetwork>> {
1146    let provider =
1147        ProviderBuilder::<<MonadEvmNetwork as FoundryEvmNetwork>::Network>::from_config(config)?
1148            .build()?;
1149    let block = provider.get_block(block_number.into()).full().await?.ok_or_else(|| {
1150        eyre::eyre!("block {block_number} is required to reconstruct transaction context")
1151    })?;
1152    BlockContext::<MonadEvmNetwork>::fetch(&provider, &block).await
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::*;
1158    use alloy_network::{AnyHeader, AnyRpcHeader};
1159    use foundry_evm::core::backend::Backend;
1160
1161    fn replay_block(transactions: Vec<AnyRpcTransaction>) -> AnyRpcBlock {
1162        AnyRpcBlock::new(
1163            alloy_rpc_types::Block::new(
1164                AnyRpcHeader::from_sealed(AnyHeader::default().seal(B256::ZERO)),
1165                BlockTransactions::Full(transactions),
1166            )
1167            .into(),
1168        )
1169    }
1170
1171    fn replay_transaction(
1172        caller: Address,
1173        nonce: u64,
1174        hash: B256,
1175        transaction_type: u8,
1176    ) -> AnyRpcTransaction {
1177        serde_json::from_value(serde_json::json!({
1178            "type": format!("0x{transaction_type:x}"),
1179            "hash": hash, "nonce": format!("0x{nonce:x}"),
1180            "from": caller, "to": Address::with_last_byte(0x43),
1181            "value": "0x7", "gas": "0x5208", "gasPrice": "0x0", "input": "0x",
1182            "v": "0x1b", "r": "0x1", "s": "0x1"
1183        }))
1184        .unwrap()
1185    }
1186
1187    #[test]
1188    fn replay_prefix_excludes_target_and_skips_system_envelopes() {
1189        let caller = Address::with_last_byte(0x42);
1190        let recipient = Address::with_last_byte(0x43);
1191        let target = B256::with_last_byte(2);
1192        let env = EvmEnvFor::<EthEvmNetwork>::default();
1193        let mut executor = ExecutorBuilder::<EthEvmNetwork>::new().build(
1194            env.clone(),
1195            TxEnvFor::<EthEvmNetwork>::default(),
1196            Backend::spawn(None).unwrap(),
1197            Default::default(),
1198        );
1199        executor.backend_mut().insert_account_info(
1200            caller,
1201            AccountInfo { balance: U256::from(100), ..Default::default() },
1202        );
1203        let block = replay_block(vec![
1204            // Unsupported envelopes from known system senders must be skipped before conversion.
1205            replay_transaction(foundry_common::MONAD_SYSTEM_ADDRESS, 0, B256::ZERO, u8::MAX),
1206            // System transaction types must also be skipped for otherwise ordinary senders.
1207            replay_transaction(caller, 0, B256::with_last_byte(4), SYSTEM_TRANSACTION_TYPE),
1208            replay_transaction(caller, 0, B256::with_last_byte(1), 0),
1209            replay_transaction(caller, 1, target, 0),
1210            replay_transaction(caller, 2, B256::with_last_byte(3), 0),
1211        ]);
1212
1213        replay_block_transactions(Some(&block), target, &mut executor, &env).unwrap();
1214
1215        assert_eq!(executor.get_balance(recipient).unwrap(), U256::from(7));
1216        assert_eq!(executor.get_nonce(caller).unwrap(), 1);
1217    }
1218
1219    #[test]
1220    fn replay_missing_target_does_not_execute_prefix() {
1221        let caller = Address::with_last_byte(0x42);
1222        let env = EvmEnvFor::<EthEvmNetwork>::default();
1223        let mut executor = ExecutorBuilder::<EthEvmNetwork>::new().build(
1224            env.clone(),
1225            TxEnvFor::<EthEvmNetwork>::default(),
1226            Backend::spawn(None).unwrap(),
1227            Default::default(),
1228        );
1229        executor.backend_mut().insert_account_info(
1230            caller,
1231            AccountInfo { balance: U256::from(100), ..Default::default() },
1232        );
1233        let block = replay_block(vec![replay_transaction(caller, 0, B256::ZERO, 0)]);
1234
1235        let error =
1236            replay_block_transactions(Some(&block), B256::with_last_byte(1), &mut executor, &env)
1237                .unwrap_err();
1238
1239        assert!(error.to_string().contains("missing from its block"), "{error:?}");
1240        assert_eq!(executor.get_nonce(caller).unwrap(), 0);
1241        assert_eq!(executor.get_balance(caller).unwrap(), U256::from(100));
1242    }
1243
1244    #[test]
1245    fn can_parse_tempo_network() {
1246        let args = VerifyBytecodeArgs::parse_from([
1247            "foundry-cli",
1248            "0x0000000000000000000000000000000000000000",
1249            "src/Counter.sol:Counter",
1250            "--network",
1251            "tempo",
1252        ]);
1253
1254        assert_eq!(args.network, Some(NetworkVariant::Tempo));
1255    }
1256
1257    #[test]
1258    #[cfg(feature = "monad")]
1259    fn can_parse_monad_network() {
1260        let args = VerifyBytecodeArgs::parse_from([
1261            "foundry-cli",
1262            "0x0000000000000000000000000000000000000000",
1263            "src/Counter.sol:Counter",
1264            "--network",
1265            "monad",
1266        ]);
1267
1268        assert_eq!(args.network, Some(NetworkVariant::Monad));
1269    }
1270
1271    #[test]
1272    fn configured_network_uses_tempo_config_network() {
1273        let config = Config { networks: NetworkVariant::Tempo.into(), ..Default::default() };
1274
1275        assert_eq!(
1276            VerifyBytecodeArgs::configured_network(None, &config),
1277            Some(NetworkVariant::Tempo)
1278        );
1279    }
1280
1281    #[test]
1282    fn configured_network_preserves_celo_execution_profile() {
1283        let mut config = Config {
1284            networks: foundry_evm_networks::NetworkConfigs::with_celo(),
1285            ..Default::default()
1286        };
1287        let endpoint_identity = ForkEndpointIdentity {
1288            endpoint: "http://localhost:8545".to_string(),
1289            execution_chain_id: 1,
1290            source_chain_id: 1,
1291            network: NetworkVariant::Tempo,
1292            network_profile: NetworkVariant::Tempo.into(),
1293            reported_hardfork: None,
1294            hardfork: None,
1295            instance_id: None,
1296            source_fork_block_number: None,
1297            source_fork_block_hash: None,
1298        };
1299
1300        assert_eq!(
1301            VerifyBytecodeArgs::configured_network(None, &config),
1302            Some(NetworkVariant::Ethereum)
1303        );
1304        assert_eq!(
1305            VerifyBytecodeArgs::materialize_execution_network(
1306                &mut config,
1307                Some(&endpoint_identity)
1308            ),
1309            NetworkVariant::Ethereum
1310        );
1311        assert!(config.networks.is_celo());
1312    }
1313
1314    #[test]
1315    fn verify_bytecode_requires_stable_endpoint_identity() {
1316        let expected = ForkEndpointIdentity {
1317            endpoint: "http://localhost:8545".to_string(),
1318            execution_chain_id: 1,
1319            source_chain_id: 1,
1320            network: NetworkVariant::Ethereum,
1321            network_profile: Default::default(),
1322            reported_hardfork: Some("FutureA".to_string()),
1323            hardfork: None,
1324            instance_id: Some(B256::with_last_byte(1)),
1325            source_fork_block_number: None,
1326            source_fork_block_hash: None,
1327        };
1328
1329        assert!(VerifyBytecodeArgs::validate_endpoint_identity(&expected, &expected).is_ok());
1330
1331        let mut reset = expected.clone();
1332        reset.instance_id = Some(B256::with_last_byte(2));
1333        assert!(VerifyBytecodeArgs::validate_endpoint_identity(&expected, &reset).is_err());
1334
1335        let mut changed_hardfork = expected.clone();
1336        changed_hardfork.reported_hardfork = Some("FutureB".to_string());
1337        assert!(
1338            VerifyBytecodeArgs::validate_endpoint_identity(&expected, &changed_hardfork).is_err()
1339        );
1340
1341        let mut evm_opts = EvmOpts::default();
1342        VerifyBytecodeArgs::apply_endpoint_expectation(&mut evm_opts, Some(&expected), true);
1343        assert_eq!(evm_opts.expected_fork_endpoint, Some(expected));
1344        assert!(evm_opts.fork_network_is_inferred);
1345    }
1346
1347    #[test]
1348    #[cfg(feature = "monad")]
1349    fn configured_network_uses_monad_config_network() {
1350        let config = Config { networks: NetworkVariant::Monad.into(), ..Default::default() };
1351
1352        assert_eq!(
1353            VerifyBytecodeArgs::configured_network(None, &config),
1354            Some(NetworkVariant::Monad)
1355        );
1356    }
1357
1358    #[test]
1359    #[cfg(feature = "monad")]
1360    fn configured_network_prefers_cli_network() {
1361        let config = Config { networks: NetworkVariant::Monad.into(), ..Default::default() };
1362
1363        assert_eq!(
1364            VerifyBytecodeArgs::configured_network(Some(NetworkVariant::Ethereum), &config),
1365            Some(NetworkVariant::Ethereum)
1366        );
1367    }
1368
1369    #[test]
1370    #[cfg(feature = "monad")]
1371    fn nested_endpoint_separates_execution_family_from_explorer_chain() {
1372        let identity = ForkEndpointIdentity {
1373            endpoint: "http://localhost:8545".to_string(),
1374            execution_chain_id: 1,
1375            source_chain_id: 143,
1376            network: NetworkVariant::Monad,
1377            network_profile: NetworkVariant::Monad.into(),
1378            reported_hardfork: None,
1379            hardfork: None,
1380            instance_id: None,
1381            source_fork_block_number: Some(123),
1382            source_fork_block_hash: None,
1383        };
1384
1385        assert_eq!(
1386            VerifyBytecodeArgs::effective_network(None, Some(&identity)),
1387            NetworkVariant::Monad
1388        );
1389        assert_eq!(VerifyBytecodeArgs::explorer_chain(None, Some(&identity)).unwrap().id(), 143);
1390        assert_eq!(
1391            VerifyBytecodeArgs::explorer_chain(Some(Chain::from_id(1)), Some(&identity))
1392                .unwrap()
1393                .id(),
1394            1
1395        );
1396    }
1397
1398    #[cfg(feature = "base")]
1399    #[test]
1400    fn configured_network_preserves_base() {
1401        let config = Config { networks: NetworkVariant::Base.into(), ..Default::default() };
1402        assert_eq!(
1403            VerifyBytecodeArgs::configured_network(None, &config),
1404            Some(NetworkVariant::Base)
1405        );
1406    }
1407}