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