forge_verify/
utils.rs

1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::DynSolValue;
3use alloy_primitives::{Address, Bytes, TxKind, U256};
4use alloy_provider::{
5    Provider,
6    network::{AnyNetwork, AnyRpcBlock},
7};
8use alloy_rpc_types::BlockId;
9use clap::ValueEnum;
10use eyre::{OptionExt, Result};
11use foundry_block_explorers::{
12    contract::{ContractCreationData, ContractMetadata, Metadata},
13    errors::EtherscanError,
14    utils::lookup_compiler_version,
15};
16use foundry_common::{
17    abi::encode_args, compile::ProjectCompiler, ignore_metadata_hash, provider::RetryProvider,
18    shell,
19};
20use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion};
21use foundry_config::Config;
22use foundry_evm::{
23    Env, EnvMut, constants::DEFAULT_CREATE2_DEPLOYER, core::AsEnvMut, executors::TracingExecutor,
24    opts::EvmOpts, traces::TraceMode, utils::apply_chain_and_block_specific_env_changes,
25};
26use foundry_evm_networks::NetworkConfigs;
27use reqwest::Url;
28use revm::{bytecode::Bytecode, database::Database, primitives::hardfork::SpecId};
29use semver::{BuildMetadata, Version};
30use serde::{Deserialize, Serialize};
31use yansi::Paint;
32
33/// Enum to represent the type of bytecode being verified
34#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
35pub enum BytecodeType {
36    #[serde(rename = "creation")]
37    Creation,
38    #[serde(rename = "runtime")]
39    Runtime,
40}
41
42impl BytecodeType {
43    /// Check if the bytecode type is creation
44    pub fn is_creation(&self) -> bool {
45        matches!(self, Self::Creation)
46    }
47
48    /// Check if the bytecode type is runtime
49    pub fn is_runtime(&self) -> bool {
50        matches!(self, Self::Runtime)
51    }
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub struct JsonResult {
56    pub bytecode_type: BytecodeType,
57    pub match_type: Option<VerificationType>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub message: Option<String>,
60}
61
62pub fn match_bytecodes(
63    local_bytecode: &[u8],
64    bytecode: &[u8],
65    constructor_args: &[u8],
66    is_runtime: bool,
67    bytecode_hash: BytecodeHash,
68) -> Option<VerificationType> {
69    // 1. Try full match
70    if local_bytecode == bytecode {
71        // If the bytecode_hash = 'none' in Config. Then it's always a partial match according to
72        // sourcify definitions. Ref: https://docs.sourcify.dev/docs/full-vs-partial-match/.
73        if bytecode_hash == BytecodeHash::None {
74            return Some(VerificationType::Partial);
75        }
76
77        Some(VerificationType::Full)
78    } else {
79        is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
80            .then_some(VerificationType::Partial)
81    }
82}
83
84pub fn build_project(
85    args: &VerifyBytecodeArgs,
86    config: &Config,
87) -> Result<CompactContractBytecode> {
88    let project = config.project()?;
89    let compiler = ProjectCompiler::new();
90
91    let mut output = compiler.compile(&project)?;
92
93    let artifact = output
94        .remove_contract(&args.contract)
95        .ok_or_eyre("Build Error: Contract artifact not found locally")?;
96
97    Ok(artifact.into_contract_bytecode())
98}
99
100pub fn build_using_cache(
101    args: &VerifyBytecodeArgs,
102    etherscan_settings: &Metadata,
103    config: &Config,
104) -> Result<CompactContractBytecode> {
105    let project = config.project()?;
106    let cache = project.read_cache_file()?;
107    let cached_artifacts = cache.read_artifacts::<CompactContractBytecode>()?;
108
109    for (key, value) in cached_artifacts {
110        let name = args.contract.name.to_owned() + ".sol";
111        let version = etherscan_settings.compiler_version.to_owned();
112        // Ignores vyper
113        if version.starts_with("vyper:") {
114            eyre::bail!("Vyper contracts are not supported")
115        }
116        // Parse etherscan version string
117        let version = version.split('+').next().unwrap_or("").trim_start_matches('v').to_string();
118
119        // Check if `out/directory` name matches the contract name
120        if key.ends_with(name.as_str()) {
121            let name = name.replace(".sol", ".json");
122            for artifact in value.into_values().flatten() {
123                // Check if ABI file matches the name
124                if !artifact.file.ends_with(&name) {
125                    continue;
126                }
127
128                // Check if Solidity version matches
129                if let Ok(version) = Version::parse(&version)
130                    && !(artifact.version.major == version.major
131                        && artifact.version.minor == version.minor
132                        && artifact.version.patch == version.patch)
133                {
134                    continue;
135                }
136
137                return Ok(artifact.artifact);
138            }
139        }
140    }
141
142    eyre::bail!("couldn't find cached artifact for contract {}", args.contract.name)
143}
144
145pub fn print_result(
146    res: Option<VerificationType>,
147    bytecode_type: BytecodeType,
148    json_results: &mut Vec<JsonResult>,
149    etherscan_config: &Metadata,
150    config: &Config,
151) {
152    if let Some(res) = res {
153        if !shell::is_json() {
154            let _ = sh_println!(
155                "{} with status {}",
156                format!("{bytecode_type:?} code matched").green().bold(),
157                res.green().bold()
158            );
159        } else {
160            let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
161            json_results.push(json_res);
162        }
163    } else if !shell::is_json() {
164        let _ = sh_err!(
165            "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
166        );
167        let mismatches = find_mismatch_in_settings(etherscan_config, config);
168        for mismatch in mismatches {
169            let _ = sh_eprintln!("{}", mismatch.red().bold());
170        }
171    } else {
172        let json_res = JsonResult {
173            bytecode_type,
174            match_type: res,
175            message: Some(format!(
176                "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
177            )),
178        };
179        json_results.push(json_res);
180    }
181}
182
183fn is_partial_match(
184    mut local_bytecode: &[u8],
185    mut bytecode: &[u8],
186    constructor_args: &[u8],
187    is_runtime: bool,
188) -> bool {
189    // 1. Check length of constructor args
190    if constructor_args.is_empty() || is_runtime {
191        // Assume metadata is at the end of the bytecode
192        return try_extract_and_compare_bytecode(local_bytecode, bytecode);
193    }
194
195    // If not runtime, extract constructor args from the end of the bytecode
196    bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
197    local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
198
199    try_extract_and_compare_bytecode(local_bytecode, bytecode)
200}
201
202fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
203    local_bytecode = ignore_metadata_hash(local_bytecode);
204    bytecode = ignore_metadata_hash(bytecode);
205
206    // Now compare the local code and bytecode
207    local_bytecode == bytecode
208}
209
210fn find_mismatch_in_settings(
211    etherscan_settings: &Metadata,
212    local_settings: &Config,
213) -> Vec<String> {
214    let mut mismatches: Vec<String> = vec![];
215    if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
216        let str = format!(
217            "EVM version mismatch: local={}, onchain={}",
218            local_settings.evm_version, etherscan_settings.evm_version
219        );
220        mismatches.push(str);
221    }
222    let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
223    if etherscan_settings.optimization_used != local_optimizer {
224        let str = format!(
225            "Optimizer mismatch: local={}, onchain={}",
226            local_settings.optimizer.unwrap_or(false),
227            etherscan_settings.optimization_used
228        );
229        mismatches.push(str);
230    }
231    if local_settings.optimizer_runs.is_some_and(|runs| etherscan_settings.runs != runs as u64)
232        || (local_settings.optimizer_runs.is_none() && etherscan_settings.runs > 0)
233    {
234        let str = format!(
235            "Optimizer runs mismatch: local={}, onchain={}",
236            local_settings.optimizer_runs.map_or("unknown".to_string(), |runs| runs.to_string()),
237            etherscan_settings.runs
238        );
239        mismatches.push(str);
240    }
241
242    mismatches
243}
244
245pub fn maybe_predeploy_contract(
246    creation_data: Result<ContractCreationData, EtherscanError>,
247) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
248    let mut maybe_predeploy = false;
249    match creation_data {
250        Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
251        // Ref: https://explorer.mode.network/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010
252        Err(EtherscanError::EmptyResult { status, message })
253            if status == "1" && message == "OK" =>
254        {
255            maybe_predeploy = true;
256            Ok((None, maybe_predeploy))
257        }
258        // Ref: https://api.basescan.org/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010&apiKey=YourAPIKey
259        Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
260            maybe_predeploy = true;
261            Ok((None, maybe_predeploy))
262        }
263        Err(e) => eyre::bail!("Error fetching creation data from verifier-url: {:?}", e),
264    }
265}
266
267pub fn check_and_encode_args(
268    artifact: &CompactContractBytecode,
269    args: Vec<String>,
270) -> Result<Vec<u8>, eyre::ErrReport> {
271    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
272        if constructor.inputs.len() != args.len() {
273            eyre::bail!(
274                "Mismatch of constructor arguments length. Expected {}, got {}",
275                constructor.inputs.len(),
276                args.len()
277            );
278        }
279        encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode())
280    } else {
281        Ok(Vec::new())
282    }
283}
284
285pub fn check_explorer_args(source_code: ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
286    if let Some(args) = source_code.items.first() {
287        Ok(args.constructor_arguments.clone())
288    } else {
289        eyre::bail!("No constructor arguments found from block explorer");
290    }
291}
292
293pub fn check_args_len(
294    artifact: &CompactContractBytecode,
295    args: &Bytes,
296) -> Result<(), eyre::ErrReport> {
297    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
298        && !constructor.inputs.is_empty()
299        && args.is_empty()
300    {
301        eyre::bail!(
302            "Contract expects {} constructor argument(s), but none were provided",
303            constructor.inputs.len()
304        );
305    }
306    Ok(())
307}
308
309pub async fn get_tracing_executor(
310    fork_config: &mut Config,
311    fork_blk_num: u64,
312    evm_version: EvmVersion,
313    evm_opts: EvmOpts,
314) -> Result<(Env, TracingExecutor)> {
315    fork_config.fork_block_number = Some(fork_blk_num);
316    fork_config.evm_version = evm_version;
317
318    let create2_deployer = evm_opts.create2_deployer;
319    let (env, fork, _chain, networks) =
320        TracingExecutor::get_fork_material(fork_config, evm_opts).await?;
321
322    let executor = TracingExecutor::new(
323        env.clone(),
324        fork,
325        Some(fork_config.evm_version),
326        TraceMode::Call,
327        networks,
328        create2_deployer,
329        None,
330    )?;
331
332    Ok((env, executor))
333}
334
335pub fn configure_env_block(env: &mut EnvMut<'_>, block: &AnyRpcBlock, config: NetworkConfigs) {
336    env.block.timestamp = U256::from(block.header.timestamp);
337    env.block.beneficiary = block.header.beneficiary;
338    env.block.difficulty = block.header.difficulty;
339    env.block.prevrandao = Some(block.header.mix_hash.unwrap_or_default());
340    env.block.basefee = block.header.base_fee_per_gas.unwrap_or_default();
341    env.block.gas_limit = block.header.gas_limit;
342    apply_chain_and_block_specific_env_changes::<AnyNetwork>(env.as_env_mut(), block, config);
343}
344
345pub fn deploy_contract(
346    executor: &mut TracingExecutor,
347    env: &Env,
348    spec_id: SpecId,
349    to: Option<TxKind>,
350) -> Result<Address, eyre::ErrReport> {
351    let env = Env::new_with_spec_id(
352        env.evm_env.cfg_env.clone(),
353        env.evm_env.block_env.clone(),
354        env.tx.clone(),
355        spec_id,
356    );
357
358    if to.is_some_and(|to| to.is_call()) {
359        let TxKind::Call(to) = to.unwrap() else { unreachable!() };
360        if to != DEFAULT_CREATE2_DEPLOYER {
361            eyre::bail!(
362                "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
363            );
364        }
365        let result = executor.transact_with_env(env)?;
366
367        trace!(transact_result = ?result.exit_reason);
368        if result.result.len() != 20 {
369            eyre::bail!(
370                "Failed to deploy contract on fork at block: call result is not exactly 20 bytes"
371            );
372        }
373
374        Ok(Address::from_slice(&result.result))
375    } else {
376        let deploy_result = executor.deploy_with_env(env, None)?;
377        trace!(deploy_result = ?deploy_result.raw.exit_reason);
378        Ok(deploy_result.address)
379    }
380}
381
382pub async fn get_runtime_codes(
383    executor: &mut TracingExecutor,
384    provider: &RetryProvider,
385    address: Address,
386    fork_address: Address,
387    block: Option<u64>,
388) -> Result<(Bytecode, Bytes)> {
389    let fork_runtime_code = executor
390        .backend_mut()
391        .basic(fork_address)?
392        .ok_or_else(|| {
393            eyre::eyre!(
394                "Failed to get runtime code for contract deployed on fork at address {}",
395                fork_address
396            )
397        })?
398        .code
399        .ok_or_else(|| {
400            eyre::eyre!(
401                "Bytecode does not exist for contract deployed on fork at address {}",
402                fork_address
403            )
404        })?;
405
406    let onchain_runtime_code = if let Some(block) = block {
407        provider.get_code_at(address).block_id(BlockId::number(block)).await?
408    } else {
409        provider.get_code_at(address).await?
410    };
411
412    Ok((fork_runtime_code, onchain_runtime_code))
413}
414
415/// Returns `true` if the URL only consists of host.
416///
417/// This is used to check user input url for missing /api path
418pub fn is_host_only(url: &Url) -> bool {
419    matches!(url.path(), "/" | "")
420}
421
422/// Given any solc [Version] return a [Version] with build metadata
423///
424/// # Example
425///
426/// ```ignore
427/// use semver::{BuildMetadata, Version};
428/// let version = Version::new(1, 2, 3);
429/// let version = ensure_solc_build_metadata(version).await?;
430/// assert_ne!(version.build, BuildMetadata::EMPTY);
431/// ```
432pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
433    if version.build != BuildMetadata::EMPTY {
434        Ok(version)
435    } else {
436        Ok(lookup_compiler_version(&version).await?)
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_host_only() {
446        assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
447        assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
448        assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
449    }
450}