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