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