forge_verify/
utils.rs

1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::DynSolValue;
3use alloy_primitives::{Address, Bytes, U256};
4use alloy_provider::{network::AnyRpcBlock, Provider};
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::{abi::encode_args, compile::ProjectCompiler, provider::RetryProvider, shell};
13use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion};
14use foundry_config::Config;
15use foundry_evm::{
16    constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, opts::EvmOpts,
17    traces::TraceMode,
18};
19use reqwest::Url;
20use revm_primitives::{
21    db::Database,
22    env::{EnvWithHandlerCfg, HandlerCfg},
23    Bytecode, Env, SpecId, TxKind,
24};
25use semver::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                    if !(artifact.version.major == version.major &&
127                        artifact.version.minor == version.minor &&
128                        artifact.version.patch == version.patch)
129                    {
130                        continue;
131                    }
132                }
133
134                return Ok(artifact.artifact)
135            }
136        }
137    }
138
139    eyre::bail!("couldn't find cached artifact for contract {}", args.contract.name)
140}
141
142pub fn print_result(
143    res: Option<VerificationType>,
144    bytecode_type: BytecodeType,
145    json_results: &mut Vec<JsonResult>,
146    etherscan_config: &Metadata,
147    config: &Config,
148) {
149    if let Some(res) = res {
150        if !shell::is_json() {
151            let _ = sh_println!(
152                "{} with status {}",
153                format!("{bytecode_type:?} code matched").green().bold(),
154                res.green().bold()
155            );
156        } else {
157            let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
158            json_results.push(json_res);
159        }
160    } else if !shell::is_json() {
161        let _ = sh_err!(
162            "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
163        );
164        let mismatches = find_mismatch_in_settings(etherscan_config, config);
165        for mismatch in mismatches {
166            let _ = sh_eprintln!("{}", mismatch.red().bold());
167        }
168    } else {
169        let json_res = JsonResult {
170            bytecode_type,
171            match_type: res,
172            message: Some(format!(
173                "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
174            )),
175        };
176        json_results.push(json_res);
177    }
178}
179
180fn is_partial_match(
181    mut local_bytecode: &[u8],
182    mut bytecode: &[u8],
183    constructor_args: &[u8],
184    is_runtime: bool,
185) -> bool {
186    // 1. Check length of constructor args
187    if constructor_args.is_empty() || is_runtime {
188        // Assume metadata is at the end of the bytecode
189        return try_extract_and_compare_bytecode(local_bytecode, bytecode)
190    }
191
192    // If not runtime, extract constructor args from the end of the bytecode
193    bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
194    local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
195
196    try_extract_and_compare_bytecode(local_bytecode, bytecode)
197}
198
199fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
200    local_bytecode = extract_metadata_hash(local_bytecode);
201    bytecode = extract_metadata_hash(bytecode);
202
203    // Now compare the local code and bytecode
204    local_bytecode == bytecode
205}
206
207/// @dev This assumes that the metadata is at the end of the bytecode
208fn extract_metadata_hash(bytecode: &[u8]) -> &[u8] {
209    // Get the last two bytes of the bytecode to find the length of CBOR metadata
210    let metadata_len = &bytecode[bytecode.len() - 2..];
211    let metadata_len = u16::from_be_bytes([metadata_len[0], metadata_len[1]]);
212
213    if metadata_len as usize <= bytecode.len() {
214        if ciborium::from_reader::<ciborium::Value, _>(
215            &bytecode[bytecode.len() - 2 - metadata_len as usize..bytecode.len() - 2],
216        )
217        .is_ok()
218        {
219            &bytecode[..bytecode.len() - 2 - metadata_len as usize]
220        } else {
221            bytecode
222        }
223    } else {
224        bytecode
225    }
226}
227
228fn find_mismatch_in_settings(
229    etherscan_settings: &Metadata,
230    local_settings: &Config,
231) -> Vec<String> {
232    let mut mismatches: Vec<String> = vec![];
233    if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
234        let str = format!(
235            "EVM version mismatch: local={}, onchain={}",
236            local_settings.evm_version, etherscan_settings.evm_version
237        );
238        mismatches.push(str);
239    }
240    let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
241    if etherscan_settings.optimization_used != local_optimizer {
242        let str = format!(
243            "Optimizer mismatch: local={}, onchain={}",
244            local_settings.optimizer.unwrap_or(false),
245            etherscan_settings.optimization_used
246        );
247        mismatches.push(str);
248    }
249    if local_settings.optimizer_runs.is_some_and(|runs| etherscan_settings.runs != runs as u64) ||
250        (local_settings.optimizer_runs.is_none() && etherscan_settings.runs > 0)
251    {
252        let str = format!(
253            "Optimizer runs mismatch: local={}, onchain={}",
254            local_settings.optimizer_runs.unwrap(),
255            etherscan_settings.runs
256        );
257        mismatches.push(str);
258    }
259
260    mismatches
261}
262
263pub fn maybe_predeploy_contract(
264    creation_data: Result<ContractCreationData, EtherscanError>,
265) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
266    let mut maybe_predeploy = false;
267    match creation_data {
268        Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
269        // Ref: https://explorer.mode.network/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010
270        Err(EtherscanError::EmptyResult { status, message })
271            if status == "1" && message == "OK" =>
272        {
273            maybe_predeploy = true;
274            Ok((None, maybe_predeploy))
275        }
276        // Ref: https://api.basescan.org/api?module=contract&action=getcontractcreation&contractaddresses=0xC0d3c0d3c0D3c0d3C0D3c0D3C0d3C0D3C0D30010&apiKey=YourAPIKey
277        Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
278            maybe_predeploy = true;
279            Ok((None, maybe_predeploy))
280        }
281        Err(e) => eyre::bail!("Error fetching creation data from verifier-url: {:?}", e),
282    }
283}
284
285pub fn check_and_encode_args(
286    artifact: &CompactContractBytecode,
287    args: Vec<String>,
288) -> Result<Vec<u8>, eyre::ErrReport> {
289    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
290        if constructor.inputs.len() != args.len() {
291            eyre::bail!(
292                "Mismatch of constructor arguments length. Expected {}, got {}",
293                constructor.inputs.len(),
294                args.len()
295            );
296        }
297        encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode())
298    } else {
299        Ok(Vec::new())
300    }
301}
302
303pub fn check_explorer_args(source_code: ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
304    if let Some(args) = source_code.items.first() {
305        Ok(args.constructor_arguments.clone())
306    } else {
307        eyre::bail!("No constructor arguments found from block explorer");
308    }
309}
310
311pub fn check_args_len(
312    artifact: &CompactContractBytecode,
313    args: &Bytes,
314) -> Result<(), eyre::ErrReport> {
315    if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
316        if !constructor.inputs.is_empty() && args.is_empty() {
317            eyre::bail!(
318                "Contract expects {} constructor argument(s), but none were provided",
319                constructor.inputs.len()
320            );
321        }
322    }
323    Ok(())
324}
325
326pub async fn get_tracing_executor(
327    fork_config: &mut Config,
328    fork_blk_num: u64,
329    evm_version: EvmVersion,
330    evm_opts: EvmOpts,
331) -> Result<(Env, TracingExecutor)> {
332    fork_config.fork_block_number = Some(fork_blk_num);
333    fork_config.evm_version = evm_version;
334
335    let create2_deployer = evm_opts.create2_deployer;
336    let (env, fork, _chain, is_odyssey) =
337        TracingExecutor::get_fork_material(fork_config, evm_opts).await?;
338
339    let executor = TracingExecutor::new(
340        env.clone(),
341        fork,
342        Some(fork_config.evm_version),
343        TraceMode::Call,
344        is_odyssey,
345        create2_deployer,
346    )?;
347
348    Ok((env, executor))
349}
350
351pub fn configure_env_block(env: &mut Env, block: &AnyRpcBlock) {
352    env.block.timestamp = U256::from(block.header.timestamp);
353    env.block.coinbase = block.header.beneficiary;
354    env.block.difficulty = block.header.difficulty;
355    env.block.prevrandao = Some(block.header.mix_hash.unwrap_or_default());
356    env.block.basefee = U256::from(block.header.base_fee_per_gas.unwrap_or_default());
357    env.block.gas_limit = U256::from(block.header.gas_limit);
358}
359
360pub fn deploy_contract(
361    executor: &mut TracingExecutor,
362    env: &Env,
363    spec_id: SpecId,
364    to: Option<TxKind>,
365) -> Result<Address, eyre::ErrReport> {
366    let env_with_handler = EnvWithHandlerCfg::new(Box::new(env.clone()), HandlerCfg::new(spec_id));
367
368    if to.is_some_and(|to| to.is_call()) {
369        let TxKind::Call(to) = to.unwrap() else { unreachable!() };
370        if to != DEFAULT_CREATE2_DEPLOYER {
371            eyre::bail!("Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx.");
372        }
373        let result = executor.transact_with_env(env_with_handler)?;
374
375        trace!(transact_result = ?result.exit_reason);
376        if result.result.len() != 20 {
377            eyre::bail!(
378                "Failed to deploy contract on fork at block: call result is not exactly 20 bytes"
379            );
380        }
381
382        Ok(Address::from_slice(&result.result))
383    } else {
384        let deploy_result = executor.deploy_with_env(env_with_handler, None)?;
385        trace!(deploy_result = ?deploy_result.raw.exit_reason);
386        Ok(deploy_result.address)
387    }
388}
389
390pub async fn get_runtime_codes(
391    executor: &mut TracingExecutor,
392    provider: &RetryProvider,
393    address: Address,
394    fork_address: Address,
395    block: Option<u64>,
396) -> Result<(Bytecode, Bytes)> {
397    let fork_runtime_code = executor
398        .backend_mut()
399        .basic(fork_address)?
400        .ok_or_else(|| {
401            eyre::eyre!(
402                "Failed to get runtime code for contract deployed on fork at address {}",
403                fork_address
404            )
405        })?
406        .code
407        .ok_or_else(|| {
408            eyre::eyre!(
409                "Bytecode does not exist for contract deployed on fork at address {}",
410                fork_address
411            )
412        })?;
413
414    let onchain_runtime_code = if let Some(block) = block {
415        provider.get_code_at(address).block_id(BlockId::number(block)).await?
416    } else {
417        provider.get_code_at(address).await?
418    };
419
420    Ok((fork_runtime_code, onchain_runtime_code))
421}
422
423/// Returns `true` if the URL only consists of host.
424///
425/// This is used to check user input url for missing /api path
426#[inline]
427pub fn is_host_only(url: &Url) -> bool {
428    matches!(url.path(), "/" | "")
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_host_only() {
437        assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
438        assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
439        assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
440    }
441}