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