Skip to main content

forge_verify/
utils.rs

1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::DynSolValue;
3use alloy_primitives::{Address, Bytes, TxKind};
4use alloy_provider::{Provider, network::BlockResponse};
5use alloy_rpc_types::BlockId;
6use clap::ValueEnum;
7use eyre::{OptionExt, Result};
8use foundry_block_explorers::{
9    contract::{ContractCreationData, ContractMetadata, Metadata},
10    errors::EtherscanError,
11    utils::lookup_compiler_version,
12};
13use foundry_cli::utils::LoadConfig;
14use foundry_common::{
15    abi::encode_args, compile::ProjectCompiler, find_matching_contract_artifact,
16    ignore_metadata_hash, shell,
17};
18use foundry_compilers::{
19    Graph,
20    artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion},
21    compilers::ParsedSource,
22    multi::{MultiCompilerLanguage, MultiCompilerParser},
23    utils::canonicalize,
24};
25use foundry_config::Config;
26use foundry_evm::{
27    constants::DEFAULT_CREATE2_DEPLOYER,
28    core::{
29        FoundryBlock as _,
30        decode::RevertDecoder,
31        evm::{BlockEnvFor, BlockResponseFor, EvmEnvFor, FoundryEvmNetwork, SpecFor, TxEnvFor},
32    },
33    executors::TracingExecutor,
34    opts::EvmOpts,
35    traces::TraceRequirements,
36    utils::{apply_chain_and_block_specific_env_changes, block_env_from_header},
37};
38use foundry_evm_networks::NetworkConfigs;
39use reqwest::Url;
40use revm::{bytecode::Bytecode, context::Block as _, database::Database};
41use semver::{BuildMetadata, Version};
42use serde::{Deserialize, Serialize};
43use yansi::Paint;
44
45/// Enum to represent the type of bytecode being verified
46#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
47pub enum BytecodeType {
48    #[serde(rename = "creation")]
49    Creation,
50    #[serde(rename = "runtime")]
51    Runtime,
52}
53
54impl BytecodeType {
55    /// Check if the bytecode type is creation
56    pub const fn is_creation(&self) -> bool {
57        matches!(self, Self::Creation)
58    }
59
60    /// Check if the bytecode type is runtime
61    pub const fn is_runtime(&self) -> bool {
62        matches!(self, Self::Runtime)
63    }
64}
65
66#[derive(Debug, Serialize, Deserialize)]
67pub struct JsonResult {
68    pub bytecode_type: BytecodeType,
69    pub match_type: Option<VerificationType>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub message: Option<String>,
72}
73
74pub fn match_bytecodes(
75    local_bytecode: &[u8],
76    bytecode: &[u8],
77    constructor_args: &[u8],
78    is_runtime: bool,
79    bytecode_hash: BytecodeHash,
80) -> Option<VerificationType> {
81    // 1. Try full match
82    if local_bytecode == bytecode {
83        // If the bytecode_hash = 'none' in Config. Then it's always a partial match according to
84        // sourcify definitions. Ref: https://docs.sourcify.dev/docs/full-vs-partial-match/.
85        if bytecode_hash == BytecodeHash::None {
86            return Some(VerificationType::Partial);
87        }
88
89        Some(VerificationType::Full)
90    } else {
91        is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
92            .then_some(VerificationType::Partial)
93    }
94}
95
96pub fn build_project(
97    args: &VerifyBytecodeArgs,
98    config: &Config,
99) -> Result<CompactContractBytecode> {
100    let project = config.project()?;
101    let compiler = ProjectCompiler::new().quiet(true);
102
103    let target_path = match args.contract.path() {
104        Some(path) => Some(canonicalize(project.root().join(path))?),
105        None => Graph::<MultiCompilerParser>::resolve(&project.paths).ok().and_then(|graph| {
106            if graph
107                .nodes
108                .iter()
109                .any(|node| matches!(node.data.language(), MultiCompilerLanguage::Vyper(_)))
110            {
111                return None;
112            }
113            let mut matches = graph.nodes.iter().filter(|node| {
114                node.data.contract_names().iter().any(|name| name == &args.contract.name)
115            });
116            let target = matches.next()?;
117            (matches.next().is_none()
118                && graph.input_nodes().any(|input| input.path() == target.path()))
119            .then(|| target.path().to_path_buf())
120        }),
121    };
122    if let Some(target_path) = target_path {
123        let mut output = compiler.files([target_path.clone()]).compile(&project)?;
124        let artifact =
125            find_matching_contract_artifact(&mut output, &target_path, Some(&args.contract.name))?;
126        return Ok(artifact.into_contract_bytecode());
127    }
128
129    let mut output = compiler.compile(&project)?;
130
131    let artifact = output
132        .remove_contract(&args.contract)
133        .ok_or_eyre("Build Error: Contract artifact not found locally")?;
134
135    Ok(artifact.into_contract_bytecode())
136}
137
138pub fn print_result(
139    res: Option<VerificationType>,
140    bytecode_type: BytecodeType,
141    json_results: &mut Vec<JsonResult>,
142    etherscan_metadata: Option<&Metadata>,
143    config: &Config,
144) {
145    if let Some(res) = res {
146        if shell::is_json() {
147            let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
148            json_results.push(json_res);
149        } else {
150            let _ = sh_println!(
151                "{} with status {}",
152                format!("{bytecode_type:?} code matched").green().bold(),
153                res.green().bold()
154            );
155        }
156    } else if !shell::is_json() {
157        let _ = sh_err!(
158            "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
159        );
160        if let Some(etherscan_metadata) = etherscan_metadata {
161            let mismatches = find_mismatch_in_settings(etherscan_metadata, config);
162            for mismatch in mismatches {
163                let _ = sh_eprintln!("{}", mismatch.red().bold());
164            }
165        }
166    } else {
167        let json_res = JsonResult {
168            bytecode_type,
169            match_type: res,
170            message: Some(format!(
171                "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
172            )),
173        };
174        json_results.push(json_res);
175    }
176}
177
178fn is_partial_match(
179    mut local_bytecode: &[u8],
180    mut bytecode: &[u8],
181    constructor_args: &[u8],
182    is_runtime: bool,
183) -> bool {
184    // 1. Check length of constructor args
185    if constructor_args.is_empty() || is_runtime {
186        // Assume metadata is at the end of the bytecode
187        return try_extract_and_compare_bytecode(local_bytecode, bytecode);
188    }
189
190    // If not runtime, extract constructor args from the end of the bytecode
191    bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
192    local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
193
194    try_extract_and_compare_bytecode(local_bytecode, bytecode)
195}
196
197fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
198    local_bytecode = ignore_metadata_hash(local_bytecode);
199    bytecode = ignore_metadata_hash(bytecode);
200
201    // Now compare the local code and bytecode
202    local_bytecode == bytecode
203}
204
205fn find_mismatch_in_settings(
206    etherscan_settings: &Metadata,
207    local_settings: &Config,
208) -> Vec<String> {
209    let mut mismatches: Vec<String> = vec![];
210    if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
211        let str = format!(
212            "EVM version mismatch: local={}, onchain={}",
213            local_settings.evm_version, etherscan_settings.evm_version
214        );
215        mismatches.push(str);
216    }
217    let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
218    if etherscan_settings.optimization_used != local_optimizer {
219        let str = format!(
220            "Optimizer mismatch: local={}, onchain={}",
221            local_settings.optimizer.unwrap_or(false),
222            etherscan_settings.optimization_used
223        );
224        mismatches.push(str);
225    }
226    if local_settings.optimizer_runs.is_some_and(|runs| etherscan_settings.runs != runs as u64)
227        || (local_settings.optimizer_runs.is_none() && etherscan_settings.runs > 0)
228    {
229        let str = format!(
230            "Optimizer runs mismatch: local={}, onchain={}",
231            local_settings.optimizer_runs.map_or("unknown".to_string(), |runs| runs.to_string()),
232            etherscan_settings.runs
233        );
234        mismatches.push(str);
235    }
236
237    mismatches
238}
239
240pub fn maybe_predeploy_contract(
241    creation_data: Result<ContractCreationData, EtherscanError>,
242) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
243    let mut maybe_predeploy = false;
244    match creation_data {
245        Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
246        // Ref: https://explorer.mode.network/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010
247        Err(EtherscanError::EmptyResult { status, message })
248            if status == "1" && message == "OK" =>
249        {
250            maybe_predeploy = true;
251            Ok((None, maybe_predeploy))
252        }
253        // Ref: https://api.basescan.org/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010&apiKey=YourAPIKey
254        Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
255            maybe_predeploy = true;
256            Ok((None, maybe_predeploy))
257        }
258        Err(e) => {
259            eyre::bail!("Error fetching creation data from verifier-url: {:?}", e);
260        }
261    }
262}
263
264pub fn check_and_encode_args(
265    artifact: &CompactContractBytecode,
266    args: Vec<String>,
267) -> Result<Vec<u8>, eyre::ErrReport> {
268    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
269        if constructor.inputs.len() != args.len() {
270            eyre::bail!(
271                "Mismatch of constructor arguments length. Expected {}, got {}",
272                constructor.inputs.len(),
273                args.len()
274            );
275        }
276        encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode())
277    } else {
278        Ok(Vec::new())
279    }
280}
281
282pub fn check_explorer_args(source_code: &ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
283    if let Some(args) = source_code.items.first() {
284        Ok(args.constructor_arguments.clone())
285    } else {
286        eyre::bail!("No constructor arguments found from block explorer");
287    }
288}
289
290pub fn check_args_len(
291    artifact: &CompactContractBytecode,
292    args: &Bytes,
293) -> Result<(), eyre::ErrReport> {
294    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
295        && !constructor.inputs.is_empty()
296        && args.is_empty()
297    {
298        eyre::bail!(
299            "Contract expects {} constructor argument(s), but none were provided",
300            constructor.inputs.len()
301        );
302    }
303    Ok(())
304}
305
306pub fn load_fork_config_and_evm_opts(config: &Config) -> Result<(Config, EvmOpts)> {
307    let chain = config.chain;
308    let mut fork_config = config.clone();
309    fork_config.chain = None;
310
311    let (mut fork_config, mut evm_opts) = fork_config.load_config_and_evm_opts()?;
312    fork_config.chain = chain;
313    if let Some(chain) = chain {
314        evm_opts.env.chain_id = Some(chain.id());
315    }
316
317    Ok((fork_config, evm_opts))
318}
319
320pub async fn get_tracing_executor<FEN>(
321    fork_config: &mut Config,
322    fork_blk_num: u64,
323    evm_version: EvmVersion,
324    evm_opts: EvmOpts,
325) -> Result<(EvmEnvFor<FEN>, TxEnvFor<FEN>, TracingExecutor<FEN>)>
326where
327    FEN: FoundryEvmNetwork,
328{
329    fork_config.fork_block_number = Some(fork_blk_num);
330    fork_config.evm_version = evm_version;
331
332    let create2_deployer = evm_opts.create2_deployer;
333    let (evm_env, tx_env, fork, _chain, networks) =
334        TracingExecutor::<FEN>::get_fork_material(fork_config, evm_opts).await?;
335
336    let executor = TracingExecutor::<FEN>::new(
337        (evm_env.clone(), tx_env.clone()),
338        fork,
339        Some(fork_config.evm_version),
340        TraceRequirements::none().with_calls(true),
341        networks,
342        create2_deployer,
343        None,
344    )?;
345
346    Ok((evm_env, tx_env, executor))
347}
348
349pub fn configure_env_block<FEN>(
350    evm_env: &mut EvmEnvFor<FEN>,
351    block: &BlockResponseFor<FEN>,
352    config: NetworkConfigs,
353) where
354    FEN: FoundryEvmNetwork,
355{
356    let number = evm_env.block_env.number();
357    evm_env.block_env = block_env_from_header::<BlockEnvFor<FEN>>(block.header());
358    evm_env.block_env.set_number(number);
359    apply_chain_and_block_specific_env_changes::<FEN::Network, _, _>(evm_env, block, config);
360}
361
362pub fn deploy_contract<FEN>(
363    executor: &mut TracingExecutor<FEN>,
364    evm_env: &EvmEnvFor<FEN>,
365    tx_env: &TxEnvFor<FEN>,
366    spec_id: SpecFor<FEN>,
367    to: TxKind,
368) -> Result<Address, eyre::ErrReport>
369where
370    FEN: FoundryEvmNetwork,
371{
372    let mut evm_env = evm_env.clone();
373    evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec_id);
374
375    if let TxKind::Call(to) = to {
376        if to != DEFAULT_CREATE2_DEPLOYER {
377            eyre::bail!(
378                "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
379            );
380        }
381        let result = executor.transact_with_env(evm_env, tx_env.clone())?;
382
383        trace!(transact_result = ?result.exit_reason);
384
385        if result.reverted {
386            let decoded_reason = if result.result.is_empty() {
387                String::new()
388            } else {
389                format!(": {}", RevertDecoder::default().decode(&result.result, result.exit_reason))
390            };
391            eyre::bail!(
392                "Failed to deploy contract via CREATE2 on fork at block{decoded_reason}.\n\
393                This typically happens when your local bytecode differs from what was actually deployed.\n\
394                Common causes:\n\
395                - Your contract source is not at the same commit used during deployment\n\
396                - Cached build artifacts are stale (try `forge clean && forge build`)\n\
397                - Compiler settings (optimizer, evm_version, via_ir) don't match the deployment"
398            );
399        }
400
401        if result.result.len() != 20 {
402            eyre::bail!(
403                "Failed to deploy contract via CREATE2 on fork at block: deployer returned {} bytes instead of 20.\n\
404                This may indicate a bytecode mismatch - ensure your source code matches the deployed contract.",
405                result.result.len()
406            );
407        }
408
409        Ok(Address::from_slice(&result.result))
410    } else {
411        let deploy_result = executor.deploy_with_env(evm_env, tx_env.clone(), None)?;
412        trace!(deploy_result = ?deploy_result.raw.exit_reason);
413        Ok(deploy_result.address)
414    }
415}
416
417pub async fn get_runtime_codes<FEN>(
418    executor: &mut TracingExecutor<FEN>,
419    provider: &impl Provider<FEN::Network>,
420    address: Address,
421    fork_address: Address,
422    block: Option<u64>,
423) -> Result<(Bytecode, Bytes)>
424where
425    FEN: FoundryEvmNetwork,
426{
427    let fork_runtime_code = executor
428        .backend_mut()
429        .basic(fork_address)?
430        .ok_or_else(|| {
431            eyre::eyre!(
432                "Failed to get runtime code for contract deployed on fork at address {}",
433                fork_address
434            )
435        })?
436        .code
437        .ok_or_else(|| {
438            eyre::eyre!(
439                "Bytecode does not exist for contract deployed on fork at address {}",
440                fork_address
441            )
442        })?;
443
444    let block_id = block.map_or_else(BlockId::latest, BlockId::number);
445    let onchain_runtime_code = provider.get_code_at(address).block_id(block_id).await?;
446
447    Ok((fork_runtime_code, onchain_runtime_code))
448}
449
450/// Returns `true` if the URL only consists of host.
451///
452/// This is used to check user input url for missing /api path
453pub fn is_host_only(url: &Url) -> bool {
454    matches!(url.path(), "/" | "")
455}
456
457/// Wraps a failed verification error with guidance when `--verifier-url` looks misconfigured for
458/// the Etherscan provider. Returns `err` untouched when no hint applies.
459///
460/// The hint only fires when the Etherscan verifier is active: it requires an API endpoint
461/// (typically `/api`). Sourcify, Blockscout, etc. accept host-only URLs, so we leave their
462/// errors alone.
463pub fn wrap_verifier_url_error(
464    err: eyre::Error,
465    verifier_url: Option<&str>,
466    using_etherscan: bool,
467) -> eyre::Error {
468    let Some(verifier_url) = verifier_url else { return err };
469    let url = match Url::parse(verifier_url) {
470        Ok(mut url) => {
471            let _ = url.set_username("");
472            let _ = url.set_password(None);
473            url.set_query(None);
474            url.set_fragment(None);
475            url
476        }
477        Err(url_err) => {
478            return err.wrap_err(format!("Invalid verifier URL provided: {url_err}"));
479        }
480    };
481    if is_host_only(&url) && using_etherscan {
482        return err.wrap_err(format!(
483            "Verifier `etherscan` requires an API endpoint, but `--verifier-url` is host-only: `{url}`.\n\
484             Fixes (pick one):\n\
485             - Append the API path, e.g. `--verifier-url {url}api`\n\
486             - Switch verifier, e.g. `--verifier sourcify` (works with host-only URLs)"
487        ));
488    }
489    err
490}
491
492/// Given any solc [Version] return a [Version] with build metadata
493///
494/// # Example
495///
496/// ```ignore
497/// use semver::{BuildMetadata, Version};
498/// let version = Version::new(1, 2, 3);
499/// let version = ensure_solc_build_metadata(version).await?;
500/// assert_ne!(version.build, BuildMetadata::EMPTY);
501/// ```
502pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
503    if version.build == BuildMetadata::EMPTY {
504        Ok(lookup_compiler_version(&version).await?)
505    } else {
506        Ok(version)
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::verify::VerifierArgs;
514    use foundry_cli::opts::EtherscanOpts;
515    use foundry_compilers::PathStyle;
516    use foundry_config::NamedChain;
517    use foundry_test_utils::TestProject;
518
519    #[test]
520    fn build_project_finds_artifact_by_relative_contract_path() {
521        let prj = TestProject::new("verify-bytecode-relative-path", PathStyle::Dapptools);
522        prj.add_source(
523            "Counter.sol",
524            r#"
525pragma solidity 0.8.16;
526
527contract Counter {
528    uint256 public number;
529}
530"#,
531        );
532        prj.add_source(
533            "Broken.sol",
534            r#"
535pragma solidity 0.8.16;
536
537contract Broken {
538    this is not valid Solidity
539}
540"#,
541        );
542
543        let mut config = Config::load_with_root(prj.root()).unwrap();
544        config.solc = Some("0.8.16".into());
545        let args = VerifyBytecodeArgs {
546            address: Address::ZERO,
547            contract: "src/Counter.sol:Counter".parse().unwrap(),
548            block: None,
549            constructor_args: None,
550            encoded_constructor_args: None,
551            constructor_args_path: None,
552            rpc_url: None,
553            network: None,
554            etherscan: EtherscanOpts::default(),
555            verifier: VerifierArgs::default(),
556            libraries: Vec::new(),
557            root: Some(prj.root().to_path_buf()),
558            ignore: None,
559        };
560
561        let artifact = build_project(&args, &config).unwrap();
562
563        assert!(artifact.bytecode.and_then(|bytecode| bytecode.into_bytes()).is_some());
564    }
565
566    #[test]
567    fn load_fork_config_and_evm_opts_serializes_chain_as_id() {
568        let config = Config { chain: Some(NamedChain::Mainnet.into()), ..Default::default() };
569
570        let (fork_config, evm_opts) = load_fork_config_and_evm_opts(&config).unwrap();
571
572        assert_eq!(fork_config.chain, Some(NamedChain::Mainnet.into()));
573        assert_eq!(evm_opts.env.chain_id, Some(1));
574    }
575
576    #[test]
577    fn test_host_only() {
578        assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
579        assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
580        assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
581    }
582
583    #[test]
584    fn wrap_verifier_url_error_passes_through_when_no_url() {
585        let err = eyre::eyre!("upstream failure");
586        let wrapped = wrap_verifier_url_error(err, None, true);
587        assert_eq!(wrapped.to_string(), "upstream failure");
588    }
589
590    #[test]
591    fn wrap_verifier_url_error_adds_hint_for_host_only_etherscan_url() {
592        let err = eyre::eyre!("upstream failure");
593        let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), true);
594        let msg = format!("{wrapped:#}");
595        assert!(msg.contains("host-only"), "message: {msg}");
596        assert!(msg.contains("--verifier-url https://contracts.tempo.xyz/api"), "message: {msg}");
597        assert!(msg.contains("--verifier sourcify"), "message: {msg}");
598    }
599
600    /// Sourcify and other non-etherscan verifiers accept host-only URLs; we must not emit the
601    /// hint for them, otherwise we would mislead the user into editing a correct URL.
602    #[test]
603    fn wrap_verifier_url_error_does_not_hint_for_non_etherscan_provider() {
604        let err = eyre::eyre!("upstream failure");
605        let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), false);
606        assert_eq!(wrapped.to_string(), "upstream failure");
607    }
608
609    #[test]
610    fn wrap_verifier_url_error_reports_invalid_url() {
611        let err = eyre::eyre!("upstream failure");
612        let wrapped = wrap_verifier_url_error(err, Some("not a url"), true);
613        let msg = format!("{wrapped:#}");
614        assert!(msg.contains("Invalid verifier URL"), "message: {msg}");
615        assert!(!msg.contains("not a url"), "message: {msg}");
616    }
617
618    #[test]
619    fn wrap_verifier_url_error_redacts_credentials_and_query() {
620        let err = eyre::eyre!("upstream failure");
621        let wrapped = wrap_verifier_url_error(
622            err,
623            Some("https://user:secret@example.com?api_key=secret"),
624            true,
625        );
626        let msg = format!("{wrapped:#}");
627        assert!(msg.contains("https://example.com/"));
628        assert!(!msg.contains("user"));
629        assert!(!msg.contains("secret"));
630        assert!(!msg.contains("api_key"));
631    }
632}