Skip to main content

forge_verify/
utils.rs

1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
3use alloy_network::{AnyNetwork, AnyRpcBlock};
4use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
5use alloy_provider::{Provider, network::BlockResponse};
6use alloy_rpc_types::BlockId;
7use clap::ValueEnum;
8use eyre::{OptionExt, Result};
9use foundry_block_explorers::{
10    contract::{ContractCreationData, ContractMetadata, Metadata},
11    errors::EtherscanError,
12    utils::lookup_compiler_version,
13};
14use foundry_cli::utils::LoadConfig;
15use foundry_common::{
16    abi::encode_args, compile::ProjectCompiler, find_matching_contract_artifact,
17    ignore_metadata_hash, shell,
18};
19use foundry_compilers::{
20    Graph,
21    artifacts::{BytecodeHash, CompactContractBytecode},
22    compilers::ParsedSource,
23    multi::{MultiCompilerLanguage, MultiCompilerParser},
24    utils::canonicalize,
25};
26use foundry_config::Config;
27use foundry_evm::{
28    constants::DEFAULT_CREATE2_DEPLOYER,
29    core::{
30        FoundryBlock as _,
31        decode::RevertDecoder,
32        evm::{BlockEnvFor, ChainFor, EvmEnvFor, FoundryEvmNetwork, TxEnvFor},
33    },
34    executors::{ExecutorBuilder, TracingExecutor},
35    opts::EvmOpts,
36    traces::TraceRequirements,
37    utils::{apply_chain_and_block_specific_env_changes_for_chain, block_env_from_header},
38};
39use foundry_evm_networks::NetworkConfigs;
40use reqwest::Url;
41use revm::{bytecode::Bytecode, context::Block as _, database::Database};
42use semver::{BuildMetadata, Version};
43use serde::{Deserialize, Serialize};
44use yansi::Paint;
45
46#[cfg(all(test, feature = "monad"))]
47use foundry_config::FoundryHardfork;
48
49/// Enum to represent the type of bytecode being verified
50#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
51pub enum BytecodeType {
52    #[serde(rename = "creation")]
53    Creation,
54    #[serde(rename = "runtime")]
55    Runtime,
56}
57
58impl BytecodeType {
59    /// Check if the bytecode type is creation
60    pub const fn is_creation(&self) -> bool {
61        matches!(self, Self::Creation)
62    }
63
64    /// Check if the bytecode type is runtime
65    pub const fn is_runtime(&self) -> bool {
66        matches!(self, Self::Runtime)
67    }
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct JsonResult {
72    pub bytecode_type: BytecodeType,
73    pub match_type: Option<VerificationType>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub message: Option<String>,
76}
77
78pub fn match_bytecodes(
79    local_bytecode: &[u8],
80    bytecode: &[u8],
81    constructor_args: &[u8],
82    is_runtime: bool,
83    bytecode_hash: BytecodeHash,
84) -> Option<VerificationType> {
85    // 1. Try full match
86    if local_bytecode == bytecode {
87        // If the bytecode_hash = 'none' in Config. Then it's always a partial match according to
88        // sourcify definitions. Ref: https://docs.sourcify.dev/docs/full-vs-partial-match/.
89        if bytecode_hash == BytecodeHash::None {
90            return Some(VerificationType::Partial);
91        }
92
93        Some(VerificationType::Full)
94    } else {
95        is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
96            .then_some(VerificationType::Partial)
97    }
98}
99
100pub fn build_project(
101    args: &VerifyBytecodeArgs,
102    config: &Config,
103) -> Result<CompactContractBytecode> {
104    let project = config.project()?;
105    let compiler = ProjectCompiler::new().quiet(true);
106
107    let target_path = match args.contract.path() {
108        Some(path) => Some(canonicalize(project.root().join(path))?),
109        None => Graph::<MultiCompilerParser>::resolve(&project.paths).ok().and_then(|graph| {
110            if graph
111                .nodes
112                .iter()
113                .any(|node| matches!(node.data.language(), MultiCompilerLanguage::Vyper(_)))
114            {
115                return None;
116            }
117            let mut matches = graph.nodes.iter().filter(|node| {
118                node.data.contract_names().iter().any(|name| name == &args.contract.name)
119            });
120            let target = matches.next()?;
121            (matches.next().is_none()
122                && graph.input_nodes().any(|input| input.path() == target.path()))
123            .then(|| target.path().to_path_buf())
124        }),
125    };
126    if let Some(target_path) = target_path {
127        let mut output = compiler.files([target_path.clone()]).compile(&project)?;
128        let artifact =
129            find_matching_contract_artifact(&mut output, &target_path, Some(&args.contract.name))?;
130        return Ok(artifact.into_contract_bytecode());
131    }
132
133    let mut output = compiler.compile(&project)?;
134
135    let artifact = output
136        .remove_contract(&args.contract)
137        .ok_or_eyre("Build Error: Contract artifact not found locally")?;
138
139    Ok(artifact.into_contract_bytecode())
140}
141
142pub fn print_result(
143    res: Option<VerificationType>,
144    bytecode_type: BytecodeType,
145    json_results: &mut Vec<JsonResult>,
146    etherscan_metadata: Option<&Metadata>,
147    config: &Config,
148) {
149    if let Some(res) = res {
150        if shell::is_json() {
151            let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
152            json_results.push(json_res);
153        } else {
154            let _ = sh_println!(
155                "{} with status {}",
156                format!("{bytecode_type:?} code matched").green().bold(),
157                res.green().bold()
158            );
159        }
160    } else if !shell::is_json() {
161        let _ = sh_err!(
162            "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
163        );
164        if let Some(etherscan_metadata) = etherscan_metadata {
165            let mismatches = find_mismatch_in_settings(etherscan_metadata, config);
166            for mismatch in mismatches {
167                let _ = sh_eprintln!("{}", mismatch.red().bold());
168            }
169        }
170    } else {
171        let json_res = JsonResult {
172            bytecode_type,
173            match_type: res,
174            message: Some(format!(
175                "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
176            )),
177        };
178        json_results.push(json_res);
179    }
180}
181
182fn is_partial_match(
183    mut local_bytecode: &[u8],
184    mut bytecode: &[u8],
185    constructor_args: &[u8],
186    is_runtime: bool,
187) -> bool {
188    // 1. Check length of constructor args
189    if constructor_args.is_empty() || is_runtime {
190        // Assume metadata is at the end of the bytecode
191        return try_extract_and_compare_bytecode(local_bytecode, bytecode);
192    }
193
194    // The constructor args are part of what is being verified: the onchain creation code must
195    // actually end with them before they can be stripped, otherwise args of the right length
196    // could never fail the comparison.
197    if !bytecode.ends_with(constructor_args) {
198        return false;
199    }
200
201    // If not runtime, extract constructor args from the end of the bytecode
202    bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
203    local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
204
205    try_extract_and_compare_bytecode(local_bytecode, bytecode)
206}
207
208fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
209    local_bytecode = ignore_metadata_hash(local_bytecode);
210    bytecode = ignore_metadata_hash(bytecode);
211
212    // Now compare the local code and bytecode
213    local_bytecode == bytecode
214}
215
216fn find_mismatch_in_settings(
217    etherscan_settings: &Metadata,
218    local_settings: &Config,
219) -> Vec<String> {
220    let mut mismatches: Vec<String> = vec![];
221    if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
222        let str = format!(
223            "EVM version mismatch: local={}, onchain={}",
224            local_settings.evm_version, etherscan_settings.evm_version
225        );
226        mismatches.push(str);
227    }
228    let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
229    if etherscan_settings.optimization_used != local_optimizer {
230        let str = format!(
231            "Optimizer mismatch: local={}, onchain={}",
232            local_settings.optimizer.unwrap_or(false),
233            etherscan_settings.optimization_used
234        );
235        mismatches.push(str);
236    }
237    // The only caller reaches this with a `Config` from `load_config`, which has run
238    // `normalize_optimizer_settings`, so `optimizer_runs` is always set by now.
239    if let Some(local_runs) = local_settings.optimizer_runs
240        && etherscan_settings.runs != local_runs as u64
241    {
242        let str = format!(
243            "Optimizer runs mismatch: local={local_runs}, onchain={}",
244            etherscan_settings.runs
245        );
246        mismatches.push(str);
247    }
248
249    mismatches
250}
251
252pub fn maybe_predeploy_contract(
253    creation_data: Result<ContractCreationData, EtherscanError>,
254) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
255    let mut maybe_predeploy = false;
256    match creation_data {
257        Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
258        // Ref: https://explorer.mode.network/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010
259        Err(EtherscanError::EmptyResult { status, message })
260            if status == "1" && message == "OK" =>
261        {
262            maybe_predeploy = true;
263            Ok((None, maybe_predeploy))
264        }
265        // Ref: https://api.basescan.org/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010&apiKey=YourAPIKey
266        Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
267            maybe_predeploy = true;
268            Ok((None, maybe_predeploy))
269        }
270        Err(e) => {
271            eyre::bail!("Error fetching creation data from verifier-url: {:?}", e);
272        }
273    }
274}
275
276pub fn check_and_encode_args(
277    artifact: &CompactContractBytecode,
278    args: Vec<String>,
279) -> Result<Vec<u8>, eyre::ErrReport> {
280    let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) else {
281        if args.is_empty() {
282            return Ok(Vec::new());
283        }
284        eyre::bail!("Contract has no constructor arguments, but arguments were provided");
285    };
286    if constructor.inputs.len() != args.len() {
287        eyre::bail!(
288            "Mismatch of constructor arguments length. Expected {}, got {}",
289            constructor.inputs.len(),
290            args.len()
291        );
292    }
293    encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode_params())
294}
295
296pub fn validate_encoded_constructor_args(
297    artifact: &CompactContractBytecode,
298    args: Vec<u8>,
299) -> Result<Vec<u8>, eyre::ErrReport> {
300    let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) else {
301        if args.is_empty() {
302            return Ok(args);
303        }
304        eyre::bail!("Contract has no constructor arguments, but encoded arguments were provided");
305    };
306    let values = constructor
307        .abi_decode_input(&args)
308        .map_err(|err| eyre::eyre!("Invalid ABI-encoded constructor arguments: {err}"))?;
309    let encoded = constructor
310        .abi_encode_input(&values)
311        .map_err(|err| eyre::eyre!("Invalid ABI-encoded constructor arguments: {err}"))?;
312    if encoded != args {
313        eyre::bail!("Constructor arguments are not canonically ABI-encoded");
314    }
315    Ok(args)
316}
317
318pub fn check_explorer_args(source_code: &ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
319    if let Some(args) = source_code.items.first() {
320        Ok(args.constructor_arguments.clone())
321    } else {
322        eyre::bail!("No constructor arguments found from block explorer");
323    }
324}
325
326pub fn check_args_len(
327    artifact: &CompactContractBytecode,
328    args: &Bytes,
329) -> Result<(), eyre::ErrReport> {
330    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
331        && !constructor.inputs.is_empty()
332        && args.is_empty()
333    {
334        eyre::bail!(
335            "Contract expects {} constructor argument(s), but none were provided",
336            constructor.inputs.len()
337        );
338    }
339    Ok(())
340}
341
342pub fn load_fork_config_and_evm_opts(config: &Config) -> Result<(Config, EvmOpts)> {
343    let chain = config.chain;
344    let mut fork_config = config.clone();
345    fork_config.chain = None;
346
347    let (mut fork_config, mut evm_opts) = fork_config.load_config_and_evm_opts()?;
348    fork_config.chain = chain;
349    if let Some(chain) = chain {
350        evm_opts.env.chain_id = Some(chain.id());
351    }
352
353    Ok((fork_config, evm_opts))
354}
355
356pub async fn get_tracing_executor<FEN>(
357    fork_config: &mut Config,
358    fork_blk_num: u64,
359    execution_blk_num: u64,
360    execution_block: Option<&AnyRpcBlock>,
361    evm_opts: EvmOpts,
362    executor_builder: ExecutorBuilder<FEN>,
363) -> Result<(EvmEnvFor<FEN>, TxEnvFor<FEN>, TracingExecutor<FEN>)>
364where
365    FEN: FoundryEvmNetwork,
366{
367    fork_config.fork_block_number = Some(fork_blk_num);
368
369    let create2_deployer = evm_opts.create2_deployer;
370    let mut fork = TracingExecutor::<FEN>::get_fork(fork_config, evm_opts).await?;
371    let context = fork.context();
372
373    fork.evm_env.block_env.set_number(U256::from(execution_blk_num));
374    if let Some(block) = execution_block {
375        configure_env_block::<FEN>(
376            &mut fork.evm_env,
377            block,
378            context.chain().id(),
379            context.networks(),
380        );
381    }
382    fork.resolve_spec(fork_config, None);
383    fork.extend_precompile_labels(fork_config);
384
385    let evm_env = fork.evm_env.clone();
386    let tx_env = fork.tx_env.clone();
387    let executor = fork.into_executor(
388        executor_builder,
389        TraceRequirements::none().with_calls(true),
390        create2_deployer,
391        None,
392    )?;
393
394    Ok((evm_env, tx_env, executor))
395}
396
397#[cfg(all(test, feature = "monad"))]
398fn resolve_runtime_spec<FEN>(
399    config: &Config,
400    source_chain_id: ChainId,
401    endpoint_hardfork: Option<FoundryHardfork>,
402    evm_env: &mut EvmEnvFor<FEN>,
403) -> Option<FoundryHardfork>
404where
405    FEN: FoundryEvmNetwork,
406{
407    TracingExecutor::<FEN>::resolve_spec_for_chain(
408        config,
409        source_chain_id,
410        endpoint_hardfork,
411        evm_env,
412        None,
413    )
414}
415
416pub fn configure_env_block<FEN>(
417    evm_env: &mut EvmEnvFor<FEN>,
418    block: &AnyRpcBlock,
419    source_chain_id: ChainId,
420    config: NetworkConfigs,
421) where
422    FEN: FoundryEvmNetwork,
423{
424    let number = evm_env.block_env.number();
425    evm_env.block_env = block_env_from_header::<BlockEnvFor<FEN>>(block.header());
426    evm_env.block_env.set_number(number);
427    apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
428        evm_env,
429        block,
430        source_chain_id,
431        config,
432    );
433}
434
435pub fn deploy_contract<FEN>(
436    executor: &mut TracingExecutor<FEN>,
437    evm_env: &EvmEnvFor<FEN>,
438    tx_env: &TxEnvFor<FEN>,
439    to: TxKind,
440    chain_context: ChainFor<FEN>,
441) -> Result<Address, eyre::ErrReport>
442where
443    FEN: FoundryEvmNetwork,
444{
445    if let TxKind::Call(to) = to {
446        if to != DEFAULT_CREATE2_DEPLOYER {
447            eyre::bail!(
448                "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
449            );
450        }
451        let result = executor.transact_with_env_and_context(
452            evm_env.clone(),
453            tx_env.clone(),
454            chain_context,
455        )?;
456
457        trace!(transact_result = ?result.exit_reason);
458
459        if result.reverted {
460            let decoded_reason = if result.result.is_empty() {
461                String::new()
462            } else {
463                format!(": {}", RevertDecoder::default().decode(&result.result, result.exit_reason))
464            };
465            eyre::bail!(
466                "Failed to deploy contract via CREATE2 on fork at block{decoded_reason}.\n\
467                This typically happens when your local bytecode differs from what was actually deployed.\n\
468                Common causes:\n\
469                - Your contract source is not at the same commit used during deployment\n\
470                - Cached build artifacts are stale (try `forge clean && forge build`)\n\
471                - Compiler settings (optimizer, evm_version, via_ir) don't match the deployment"
472            );
473        }
474
475        if result.result.len() != 20 {
476            eyre::bail!(
477                "Failed to deploy contract via CREATE2 on fork at block: deployer returned {} bytes instead of 20.\n\
478                This may indicate a bytecode mismatch - ensure your source code matches the deployed contract.",
479                result.result.len()
480            );
481        }
482
483        Ok(Address::from_slice(&result.result))
484    } else {
485        let deploy_result = executor.deploy_with_env_and_context(
486            evm_env.clone(),
487            tx_env.clone(),
488            chain_context,
489            None,
490        )?;
491        trace!(deploy_result = ?deploy_result.raw.exit_reason);
492        Ok(deploy_result.address)
493    }
494}
495
496pub async fn get_runtime_codes<FEN>(
497    executor: &mut TracingExecutor<FEN>,
498    provider: &impl Provider<AnyNetwork>,
499    address: Address,
500    fork_address: Address,
501    block: Option<u64>,
502) -> Result<(Bytecode, Bytes)>
503where
504    FEN: FoundryEvmNetwork,
505{
506    let fork_runtime_code = executor
507        .backend_mut()
508        .basic(fork_address)?
509        .ok_or_else(|| {
510            eyre::eyre!(
511                "Failed to get runtime code for contract deployed on fork at address {}",
512                fork_address
513            )
514        })?
515        .code
516        .ok_or_else(|| {
517            eyre::eyre!(
518                "Bytecode does not exist for contract deployed on fork at address {}",
519                fork_address
520            )
521        })?;
522
523    let block_id = block.map_or_else(BlockId::latest, BlockId::number);
524    let onchain_runtime_code = provider.get_code_at(address).block_id(block_id).await?;
525
526    Ok((fork_runtime_code, onchain_runtime_code))
527}
528
529/// Returns `true` if the URL only consists of host.
530///
531/// This is used to check user input url for missing /api path
532pub fn is_host_only(url: &Url) -> bool {
533    matches!(url.path(), "/" | "")
534}
535
536/// Wraps a failed verification error with guidance when `--verifier-url` looks misconfigured for
537/// the Etherscan provider. Returns `err` untouched when no hint applies.
538///
539/// The hint only fires when the Etherscan verifier is active: it requires an API endpoint
540/// (typically `/api`). Sourcify, Blockscout, etc. accept host-only URLs, so we leave their
541/// errors alone.
542pub fn wrap_verifier_url_error(
543    err: eyre::Error,
544    verifier_url: Option<&str>,
545    using_etherscan: bool,
546) -> eyre::Error {
547    let Some(verifier_url) = verifier_url else { return err };
548    let url = match Url::parse(verifier_url) {
549        Ok(mut url) => {
550            let _ = url.set_username("");
551            let _ = url.set_password(None);
552            url.set_query(None);
553            url.set_fragment(None);
554            url
555        }
556        Err(url_err) => {
557            return err.wrap_err(format!("Invalid verifier URL provided: {url_err}"));
558        }
559    };
560    if is_host_only(&url) && using_etherscan {
561        return err.wrap_err(format!(
562            "Verifier `etherscan` requires an API endpoint, but `--verifier-url` is host-only: `{url}`.\n\
563             Fixes (pick one):\n\
564             - Append the API path, e.g. `--verifier-url {url}api`\n\
565             - Switch verifier, e.g. `--verifier sourcify` (works with host-only URLs)"
566        ));
567    }
568    err
569}
570
571/// Given any solc [Version] return a [Version] with build metadata
572///
573/// # Example
574///
575/// ```ignore
576/// use semver::{BuildMetadata, Version};
577/// let version = Version::new(1, 2, 3);
578/// let version = ensure_solc_build_metadata(version).await?;
579/// assert_ne!(version.build, BuildMetadata::EMPTY);
580/// ```
581pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
582    if version.build == BuildMetadata::EMPTY {
583        Ok(lookup_compiler_version(&version).await?)
584    } else {
585        Ok(version)
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::verify::VerifierArgs;
593    use foundry_cli::opts::EtherscanOpts;
594    use foundry_compilers::PathStyle;
595    use foundry_config::NamedChain;
596    use foundry_test_utils::TestProject;
597
598    #[cfg(feature = "monad")]
599    fn monad_env(timestamp: u64) -> EvmEnvFor<foundry_evm::core::evm::MonadEvmNetwork> {
600        let mut env = EvmEnvFor::<foundry_evm::core::evm::MonadEvmNetwork>::default();
601        env.cfg_env.chain_id = NamedChain::Monad as u64;
602        env.block_env.set_timestamp(U256::from(timestamp));
603        env
604    }
605
606    #[test]
607    fn encoded_constructor_args_must_be_canonical() {
608        let artifact = CompactContractBytecode {
609            abi: Some(alloy_json_abi::JsonAbi::parse(["constructor(uint256 value)"]).unwrap()),
610            bytecode: None,
611            deployed_bytecode: None,
612        };
613        let args = artifact
614            .abi
615            .as_ref()
616            .unwrap()
617            .constructor()
618            .unwrap()
619            .abi_encode_input(&[DynSolValue::Uint(U256::from(1), 256)])
620            .unwrap();
621
622        assert_eq!(validate_encoded_constructor_args(&artifact, args.clone()).unwrap(), args);
623
624        // Arbitrary bytes prepended to valid arguments can overlap the creation bytecode's
625        // metadata and must not be accepted as part of the constructor arguments.
626        let overlapping = [alloy_primitives::hex!("a1616101").as_slice(), &args].concat();
627        assert!(validate_encoded_constructor_args(&artifact, overlapping).is_err());
628    }
629
630    #[test]
631    fn typed_constructor_args_require_a_constructor() {
632        let artifact = CompactContractBytecode {
633            abi: Some(alloy_json_abi::JsonAbi::default()),
634            bytecode: None,
635            deployed_bytecode: None,
636        };
637
638        assert!(check_and_encode_args(&artifact, vec!["1".to_string()]).is_err());
639        assert_eq!(check_and_encode_args(&artifact, Vec::new()).unwrap(), Vec::<u8>::new());
640    }
641
642    #[test]
643    fn dynamic_constructor_args_are_encoded_as_top_level_params() {
644        let artifact = CompactContractBytecode {
645            abi: Some(alloy_json_abi::JsonAbi::parse(["constructor(string value)"]).unwrap()),
646            bytecode: None,
647            deployed_bytecode: None,
648        };
649
650        let encoded = check_and_encode_args(&artifact, vec!["hi".to_string()]).unwrap();
651
652        // Constructor arguments are encoded as top-level ABI parameters, so the first word is the
653        // offset to the string payload.
654        let expected = alloy_primitives::hex!(
655            "0000000000000000000000000000000000000000000000000000000000000020"
656            "0000000000000000000000000000000000000000000000000000000000000002"
657            "6869000000000000000000000000000000000000000000000000000000000000"
658        );
659        assert_eq!(encoded, expected);
660    }
661
662    #[test]
663    fn creation_code_must_end_with_constructor_args() {
664        let code = alloy_primitives::hex!("6080604052348015600e575f5ffd5b50607b80601a5f395ff3fe");
665        let real_args = [0x11u8; 32];
666        let wrong_args = [0x22u8; 32];
667
668        let onchain = [code.as_slice(), &real_args].concat();
669
670        // Wrong args of the right length used to report a partial match because the tails were
671        // stripped from both sides without comparing them.
672        let local = [code.as_slice(), &wrong_args].concat();
673        assert_eq!(match_bytecodes(&local, &onchain, &wrong_args, false, BytecodeHash::Ipfs), None);
674
675        let local = [code.as_slice(), &real_args].concat();
676        assert_eq!(
677            match_bytecodes(&local, &onchain, &real_args, false, BytecodeHash::Ipfs),
678            Some(VerificationType::Full)
679        );
680
681        // A valid dynamic encoding can end with another valid encoding. The suffix alone must
682        // not establish that the supplied arguments match.
683        let suffix_args =
684            DynSolValue::Tuple(vec![DynSolValue::Bytes(real_args.to_vec())]).abi_encode();
685        let deployment_args =
686            DynSolValue::Tuple(vec![DynSolValue::Bytes(suffix_args.clone())]).abi_encode();
687        assert!(deployment_args.ends_with(&suffix_args));
688
689        let onchain = [code.as_slice(), deployment_args.as_slice()].concat();
690        let local = [code.as_slice(), suffix_args.as_slice()].concat();
691        assert_eq!(
692            match_bytecodes(&local, &onchain, &suffix_args, false, BytecodeHash::Ipfs),
693            None
694        );
695    }
696
697    #[test]
698    fn build_project_finds_artifact_by_relative_contract_path() {
699        let prj = TestProject::new("verify-bytecode-relative-path", PathStyle::Dapptools);
700        prj.add_source(
701            "Counter.sol",
702            r#"
703pragma solidity 0.8.16;
704
705contract Counter {
706    uint256 public number;
707}
708"#,
709        );
710        prj.add_source(
711            "Broken.sol",
712            r#"
713pragma solidity 0.8.16;
714
715contract Broken {
716    this is not valid Solidity
717}
718"#,
719        );
720
721        let mut config = Config::load_with_root(prj.root()).unwrap();
722        config.solc = Some("0.8.16".into());
723        let args = VerifyBytecodeArgs {
724            address: Address::ZERO,
725            contract: "src/Counter.sol:Counter".parse().unwrap(),
726            block: None,
727            constructor_args: None,
728            encoded_constructor_args: None,
729            constructor_args_path: None,
730            rpc_url: None,
731            network: None,
732            etherscan: EtherscanOpts::default(),
733            verifier: VerifierArgs::default(),
734            libraries: Vec::new(),
735            root: Some(prj.root().to_path_buf()),
736            ignore: None,
737        };
738
739        let artifact = build_project(&args, &config).unwrap();
740
741        assert!(artifact.bytecode.and_then(|bytecode| bytecode.into_bytes()).is_some());
742    }
743
744    #[test]
745    fn load_fork_config_and_evm_opts_serializes_chain_as_id() {
746        let config = Config { chain: Some(NamedChain::Mainnet.into()), ..Default::default() };
747
748        let (fork_config, evm_opts) = load_fork_config_and_evm_opts(&config).unwrap();
749
750        assert_eq!(fork_config.chain, Some(NamedChain::Mainnet.into()));
751        assert_eq!(evm_opts.env.chain_id, Some(1));
752    }
753
754    #[test]
755    #[cfg(feature = "monad")]
756    fn runtime_spec_uses_monad_source_chain_timestamp() {
757        let monad_nine_timestamp = foundry_evm::hardforks::MonadHardfork::MonadNine
758            .mainnet_activation_timestamp()
759            .unwrap();
760
761        let before_config = Config {
762            evm_version: foundry_compilers::artifacts::EvmVersion::Osaka,
763            ..Default::default()
764        };
765        let mut before_env = monad_env(monad_nine_timestamp - 1);
766        before_env.cfg_env.chain_id = NamedChain::Mainnet as u64;
767        let before = resolve_runtime_spec::<foundry_evm::core::evm::MonadEvmNetwork>(
768            &before_config,
769            NamedChain::Monad as u64,
770            None,
771            &mut before_env,
772        );
773
774        assert_eq!(
775            before,
776            Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadEight))
777        );
778        assert_eq!(before_env.cfg_env.spec, foundry_evm::hardforks::MonadHardfork::MonadEight);
779        assert_eq!(before_env.cfg_env.chain_id, NamedChain::Mainnet as u64);
780
781        let after_config = Config {
782            evm_version: foundry_compilers::artifacts::EvmVersion::Prague,
783            ..Default::default()
784        };
785        let mut after_env = monad_env(monad_nine_timestamp);
786        let after = resolve_runtime_spec::<foundry_evm::core::evm::MonadEvmNetwork>(
787            &after_config,
788            NamedChain::Monad as u64,
789            None,
790            &mut after_env,
791        );
792
793        assert_eq!(
794            after,
795            Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadNine))
796        );
797        assert_eq!(after_env.cfg_env.spec, foundry_evm::hardforks::MonadHardfork::MonadNine);
798    }
799
800    #[test]
801    #[cfg(feature = "monad")]
802    fn runtime_spec_and_labels_prefer_explicit_monad_hardfork() {
803        let mut config = Config {
804            hardfork: Some(foundry_evm::hardforks::MonadHardfork::MonadEight.into()),
805            ..Default::default()
806        };
807        let mut env = monad_env(
808            foundry_evm::hardforks::MonadHardfork::MonadNine
809                .mainnet_activation_timestamp()
810                .unwrap(),
811        );
812        let networks = NetworkConfigs::with_monad();
813
814        let resolved = resolve_runtime_spec::<foundry_evm::core::evm::MonadEvmNetwork>(
815            &config,
816            NamedChain::Monad as u64,
817            Some(foundry_evm::hardforks::MonadHardfork::MonadNine.into()),
818            &mut env,
819        );
820        TracingExecutor::<foundry_evm::core::evm::MonadEvmNetwork>::extend_precompile_labels(
821            &mut config,
822            networks,
823            resolved,
824        );
825
826        assert_eq!(
827            resolved,
828            Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadEight))
829        );
830        assert_eq!(env.cfg_env.spec, foundry_evm::hardforks::MonadHardfork::MonadEight);
831        assert!(config.labels.values().any(|label| label == "Staking"));
832        assert!(!config.labels.values().any(|label| label == "ReserveBalance"));
833    }
834
835    #[test]
836    fn test_host_only() {
837        assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
838        assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
839        assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
840    }
841
842    #[test]
843    fn wrap_verifier_url_error_passes_through_when_no_url() {
844        let err = eyre::eyre!("upstream failure");
845        let wrapped = wrap_verifier_url_error(err, None, true);
846        assert_eq!(wrapped.to_string(), "upstream failure");
847    }
848
849    #[test]
850    fn wrap_verifier_url_error_adds_hint_for_host_only_etherscan_url() {
851        let err = eyre::eyre!("upstream failure");
852        let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), true);
853        let msg = format!("{wrapped:#}");
854        assert!(msg.contains("host-only"), "message: {msg}");
855        assert!(msg.contains("--verifier-url https://contracts.tempo.xyz/api"), "message: {msg}");
856        assert!(msg.contains("--verifier sourcify"), "message: {msg}");
857    }
858
859    /// Sourcify and other non-etherscan verifiers accept host-only URLs; we must not emit the
860    /// hint for them, otherwise we would mislead the user into editing a correct URL.
861    #[test]
862    fn wrap_verifier_url_error_does_not_hint_for_non_etherscan_provider() {
863        let err = eyre::eyre!("upstream failure");
864        let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), false);
865        assert_eq!(wrapped.to_string(), "upstream failure");
866    }
867
868    #[test]
869    fn wrap_verifier_url_error_reports_invalid_url() {
870        let err = eyre::eyre!("upstream failure");
871        let wrapped = wrap_verifier_url_error(err, Some("not a url"), true);
872        let msg = format!("{wrapped:#}");
873        assert!(msg.contains("Invalid verifier URL"), "message: {msg}");
874        assert!(!msg.contains("not a url"), "message: {msg}");
875    }
876
877    #[test]
878    fn wrap_verifier_url_error_redacts_credentials_and_query() {
879        let err = eyre::eyre!("upstream failure");
880        let wrapped = wrap_verifier_url_error(
881            err,
882            Some("https://user:secret@example.com?api_key=secret"),
883            true,
884        );
885        let msg = format!("{wrapped:#}");
886        assert!(msg.contains("https://example.com/"));
887        assert!(!msg.contains("user"));
888        assert!(!msg.contains("secret"));
889        assert!(!msg.contains("api_key"));
890    }
891}