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