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