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