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