1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::DynSolValue;
3use alloy_primitives::{Address, Bytes, TxKind};
4use alloy_provider::{Provider, network::BlockResponse};
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 utils::lookup_compiler_version,
12};
13use foundry_cli::utils::LoadConfig;
14use foundry_common::{
15 abi::encode_args, compile::ProjectCompiler, find_matching_contract_artifact,
16 ignore_metadata_hash, shell,
17};
18use foundry_compilers::{
19 artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion},
20 utils::canonicalize,
21};
22use foundry_config::Config;
23use foundry_evm::{
24 constants::DEFAULT_CREATE2_DEPLOYER,
25 core::{
26 FoundryBlock as _,
27 decode::RevertDecoder,
28 evm::{BlockEnvFor, BlockResponseFor, EvmEnvFor, FoundryEvmNetwork, SpecFor, TxEnvFor},
29 },
30 executors::TracingExecutor,
31 opts::EvmOpts,
32 traces::TraceRequirements,
33 utils::{apply_chain_and_block_specific_env_changes, block_env_from_header},
34};
35use foundry_evm_networks::NetworkConfigs;
36use reqwest::Url;
37use revm::{bytecode::Bytecode, context::Block as _, database::Database};
38use semver::{BuildMetadata, Version};
39use serde::{Deserialize, Serialize};
40use yansi::Paint;
41
42#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
44pub enum BytecodeType {
45 #[serde(rename = "creation")]
46 Creation,
47 #[serde(rename = "runtime")]
48 Runtime,
49}
50
51impl BytecodeType {
52 pub const fn is_creation(&self) -> bool {
54 matches!(self, Self::Creation)
55 }
56
57 pub const fn is_runtime(&self) -> bool {
59 matches!(self, Self::Runtime)
60 }
61}
62
63#[derive(Debug, Serialize, Deserialize)]
64pub struct JsonResult {
65 pub bytecode_type: BytecodeType,
66 pub match_type: Option<VerificationType>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub message: Option<String>,
69}
70
71pub fn match_bytecodes(
72 local_bytecode: &[u8],
73 bytecode: &[u8],
74 constructor_args: &[u8],
75 is_runtime: bool,
76 bytecode_hash: BytecodeHash,
77) -> Option<VerificationType> {
78 if local_bytecode == bytecode {
80 if bytecode_hash == BytecodeHash::None {
83 return Some(VerificationType::Partial);
84 }
85
86 Some(VerificationType::Full)
87 } else {
88 is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
89 .then_some(VerificationType::Partial)
90 }
91}
92
93pub fn build_project(
94 args: &VerifyBytecodeArgs,
95 config: &Config,
96) -> Result<CompactContractBytecode> {
97 let project = config.project()?;
98 let compiler = ProjectCompiler::new().quiet(true);
99
100 if let Some(path) = args.contract.path() {
101 let target_path = canonicalize(project.root().join(path))?;
102 let mut output = compiler.files([target_path.clone()]).compile(&project)?;
103 let artifact =
104 find_matching_contract_artifact(&mut output, &target_path, Some(&args.contract.name))?;
105 return Ok(artifact.into_contract_bytecode());
106 }
107
108 let mut output = compiler.compile(&project)?;
109
110 let artifact = output
111 .remove_contract(&args.contract)
112 .ok_or_eyre("Build Error: Contract artifact not found locally")?;
113
114 Ok(artifact.into_contract_bytecode())
115}
116
117pub fn print_result(
118 res: Option<VerificationType>,
119 bytecode_type: BytecodeType,
120 json_results: &mut Vec<JsonResult>,
121 etherscan_metadata: Option<&Metadata>,
122 config: &Config,
123) {
124 if let Some(res) = res {
125 if shell::is_json() {
126 let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
127 json_results.push(json_res);
128 } else {
129 let _ = sh_println!(
130 "{} with status {}",
131 format!("{bytecode_type:?} code matched").green().bold(),
132 res.green().bold()
133 );
134 }
135 } else if !shell::is_json() {
136 let _ = sh_err!(
137 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
138 );
139 if let Some(etherscan_metadata) = etherscan_metadata {
140 let mismatches = find_mismatch_in_settings(etherscan_metadata, config);
141 for mismatch in mismatches {
142 let _ = sh_eprintln!("{}", mismatch.red().bold());
143 }
144 }
145 } else {
146 let json_res = JsonResult {
147 bytecode_type,
148 match_type: res,
149 message: Some(format!(
150 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
151 )),
152 };
153 json_results.push(json_res);
154 }
155}
156
157fn is_partial_match(
158 mut local_bytecode: &[u8],
159 mut bytecode: &[u8],
160 constructor_args: &[u8],
161 is_runtime: bool,
162) -> bool {
163 if constructor_args.is_empty() || is_runtime {
165 return try_extract_and_compare_bytecode(local_bytecode, bytecode);
167 }
168
169 bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
171 local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
172
173 try_extract_and_compare_bytecode(local_bytecode, bytecode)
174}
175
176fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
177 local_bytecode = ignore_metadata_hash(local_bytecode);
178 bytecode = ignore_metadata_hash(bytecode);
179
180 local_bytecode == bytecode
182}
183
184fn find_mismatch_in_settings(
185 etherscan_settings: &Metadata,
186 local_settings: &Config,
187) -> Vec<String> {
188 let mut mismatches: Vec<String> = vec![];
189 if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
190 let str = format!(
191 "EVM version mismatch: local={}, onchain={}",
192 local_settings.evm_version, etherscan_settings.evm_version
193 );
194 mismatches.push(str);
195 }
196 let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
197 if etherscan_settings.optimization_used != local_optimizer {
198 let str = format!(
199 "Optimizer mismatch: local={}, onchain={}",
200 local_settings.optimizer.unwrap_or(false),
201 etherscan_settings.optimization_used
202 );
203 mismatches.push(str);
204 }
205 if local_settings.optimizer_runs.is_some_and(|runs| etherscan_settings.runs != runs as u64)
206 || (local_settings.optimizer_runs.is_none() && etherscan_settings.runs > 0)
207 {
208 let str = format!(
209 "Optimizer runs mismatch: local={}, onchain={}",
210 local_settings.optimizer_runs.map_or("unknown".to_string(), |runs| runs.to_string()),
211 etherscan_settings.runs
212 );
213 mismatches.push(str);
214 }
215
216 mismatches
217}
218
219pub fn maybe_predeploy_contract(
220 creation_data: Result<ContractCreationData, EtherscanError>,
221) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
222 let mut maybe_predeploy = false;
223 match creation_data {
224 Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
225 Err(EtherscanError::EmptyResult { status, message })
227 if status == "1" && message == "OK" =>
228 {
229 maybe_predeploy = true;
230 Ok((None, maybe_predeploy))
231 }
232 Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
234 maybe_predeploy = true;
235 Ok((None, maybe_predeploy))
236 }
237 Err(e) => {
238 eyre::bail!("Error fetching creation data from verifier-url: {:?}", e);
239 }
240 }
241}
242
243pub fn check_and_encode_args(
244 artifact: &CompactContractBytecode,
245 args: Vec<String>,
246) -> Result<Vec<u8>, eyre::ErrReport> {
247 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
248 if constructor.inputs.len() != args.len() {
249 eyre::bail!(
250 "Mismatch of constructor arguments length. Expected {}, got {}",
251 constructor.inputs.len(),
252 args.len()
253 );
254 }
255 encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode())
256 } else {
257 Ok(Vec::new())
258 }
259}
260
261pub fn check_explorer_args(source_code: &ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
262 if let Some(args) = source_code.items.first() {
263 Ok(args.constructor_arguments.clone())
264 } else {
265 eyre::bail!("No constructor arguments found from block explorer");
266 }
267}
268
269pub fn check_args_len(
270 artifact: &CompactContractBytecode,
271 args: &Bytes,
272) -> Result<(), eyre::ErrReport> {
273 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
274 && !constructor.inputs.is_empty()
275 && args.is_empty()
276 {
277 eyre::bail!(
278 "Contract expects {} constructor argument(s), but none were provided",
279 constructor.inputs.len()
280 );
281 }
282 Ok(())
283}
284
285pub fn load_fork_config_and_evm_opts(config: &Config) -> Result<(Config, EvmOpts)> {
286 let chain = config.chain;
287 let mut fork_config = config.clone();
288 fork_config.chain = None;
289
290 let (mut fork_config, mut evm_opts) = fork_config.load_config_and_evm_opts()?;
291 fork_config.chain = chain;
292 if let Some(chain) = chain {
293 evm_opts.env.chain_id = Some(chain.id());
294 }
295
296 Ok((fork_config, evm_opts))
297}
298
299pub async fn get_tracing_executor<FEN>(
300 fork_config: &mut Config,
301 fork_blk_num: u64,
302 evm_version: EvmVersion,
303 evm_opts: EvmOpts,
304) -> Result<(EvmEnvFor<FEN>, TxEnvFor<FEN>, TracingExecutor<FEN>)>
305where
306 FEN: FoundryEvmNetwork,
307{
308 fork_config.fork_block_number = Some(fork_blk_num);
309 fork_config.evm_version = evm_version;
310
311 let create2_deployer = evm_opts.create2_deployer;
312 let (evm_env, tx_env, fork, _chain, networks) =
313 TracingExecutor::<FEN>::get_fork_material(fork_config, evm_opts).await?;
314
315 let executor = TracingExecutor::<FEN>::new(
316 (evm_env.clone(), tx_env.clone()),
317 fork,
318 Some(fork_config.evm_version),
319 TraceRequirements::none().with_calls(true),
320 networks,
321 create2_deployer,
322 None,
323 )?;
324
325 Ok((evm_env, tx_env, executor))
326}
327
328pub fn configure_env_block<FEN>(
329 evm_env: &mut EvmEnvFor<FEN>,
330 block: &BlockResponseFor<FEN>,
331 config: NetworkConfigs,
332) where
333 FEN: FoundryEvmNetwork,
334{
335 let number = evm_env.block_env.number();
336 evm_env.block_env = block_env_from_header::<BlockEnvFor<FEN>>(block.header());
337 evm_env.block_env.set_number(number);
338 apply_chain_and_block_specific_env_changes::<FEN::Network, _, _>(evm_env, block, config);
339}
340
341pub fn deploy_contract<FEN>(
342 executor: &mut TracingExecutor<FEN>,
343 evm_env: &EvmEnvFor<FEN>,
344 tx_env: &TxEnvFor<FEN>,
345 spec_id: SpecFor<FEN>,
346 to: TxKind,
347) -> Result<Address, eyre::ErrReport>
348where
349 FEN: FoundryEvmNetwork,
350{
351 let mut evm_env = evm_env.clone();
352 evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec_id);
353
354 if let TxKind::Call(to) = to {
355 if to != DEFAULT_CREATE2_DEPLOYER {
356 eyre::bail!(
357 "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
358 );
359 }
360 let result = executor.transact_with_env(evm_env, tx_env.clone())?;
361
362 trace!(transact_result = ?result.exit_reason);
363
364 if result.reverted {
365 let decoded_reason = if result.result.is_empty() {
366 String::new()
367 } else {
368 format!(": {}", RevertDecoder::default().decode(&result.result, result.exit_reason))
369 };
370 eyre::bail!(
371 "Failed to deploy contract via CREATE2 on fork at block{decoded_reason}.\n\
372 This typically happens when your local bytecode differs from what was actually deployed.\n\
373 Common causes:\n\
374 - Your contract source is not at the same commit used during deployment\n\
375 - Cached build artifacts are stale (try `forge clean && forge build`)\n\
376 - Compiler settings (optimizer, evm_version, via_ir) don't match the deployment"
377 );
378 }
379
380 if result.result.len() != 20 {
381 eyre::bail!(
382 "Failed to deploy contract via CREATE2 on fork at block: deployer returned {} bytes instead of 20.\n\
383 This may indicate a bytecode mismatch - ensure your source code matches the deployed contract.",
384 result.result.len()
385 );
386 }
387
388 Ok(Address::from_slice(&result.result))
389 } else {
390 let deploy_result = executor.deploy_with_env(evm_env, tx_env.clone(), None)?;
391 trace!(deploy_result = ?deploy_result.raw.exit_reason);
392 Ok(deploy_result.address)
393 }
394}
395
396pub async fn get_runtime_codes<FEN>(
397 executor: &mut TracingExecutor<FEN>,
398 provider: &impl Provider<FEN::Network>,
399 address: Address,
400 fork_address: Address,
401 block: Option<u64>,
402) -> Result<(Bytecode, Bytes)>
403where
404 FEN: FoundryEvmNetwork,
405{
406 let fork_runtime_code = executor
407 .backend_mut()
408 .basic(fork_address)?
409 .ok_or_else(|| {
410 eyre::eyre!(
411 "Failed to get runtime code for contract deployed on fork at address {}",
412 fork_address
413 )
414 })?
415 .code
416 .ok_or_else(|| {
417 eyre::eyre!(
418 "Bytecode does not exist for contract deployed on fork at address {}",
419 fork_address
420 )
421 })?;
422
423 let block_id = block.map_or_else(BlockId::latest, BlockId::number);
424 let onchain_runtime_code = provider.get_code_at(address).block_id(block_id).await?;
425
426 Ok((fork_runtime_code, onchain_runtime_code))
427}
428
429pub fn is_host_only(url: &Url) -> bool {
433 matches!(url.path(), "/" | "")
434}
435
436pub fn wrap_verifier_url_error(
443 err: eyre::Error,
444 verifier_url: Option<&str>,
445 using_etherscan: bool,
446) -> eyre::Error {
447 let Some(verifier_url) = verifier_url else { return err };
448 let url = match Url::parse(verifier_url) {
449 Ok(url) => url,
450 Err(url_err) => {
451 return err.wrap_err(format!("Invalid URL {verifier_url} provided: {url_err}"));
452 }
453 };
454 if is_host_only(&url) && using_etherscan {
455 return err.wrap_err(format!(
456 "Verifier `etherscan` requires an API endpoint, but `--verifier-url` is host-only: `{verifier_url}`.\n\
457 Fixes (pick one):\n\
458 - Append the API path, e.g. `--verifier-url {verifier_url}/api`\n\
459 - Switch verifier, e.g. `--verifier sourcify` (works with host-only URLs)"
460 ));
461 }
462 err
463}
464
465pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
476 if version.build == BuildMetadata::EMPTY {
477 Ok(lookup_compiler_version(&version).await?)
478 } else {
479 Ok(version)
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use crate::verify::VerifierArgs;
487 use foundry_cli::opts::EtherscanOpts;
488 use foundry_compilers::PathStyle;
489 use foundry_config::NamedChain;
490 use foundry_test_utils::TestProject;
491
492 #[test]
493 fn build_project_finds_artifact_by_relative_contract_path() {
494 let prj = TestProject::new("verify-bytecode-relative-path", PathStyle::Dapptools);
495 prj.add_source(
496 "Counter.sol",
497 r#"
498pragma solidity 0.8.16;
499
500contract Counter {
501 uint256 public number;
502}
503"#,
504 );
505 prj.add_source(
506 "Broken.sol",
507 r#"
508pragma solidity 0.8.16;
509
510contract Broken {
511 this is not valid Solidity
512}
513"#,
514 );
515
516 let mut config = Config::load_with_root(prj.root()).unwrap();
517 config.solc = Some("0.8.16".into());
518 let args = VerifyBytecodeArgs {
519 address: Address::ZERO,
520 contract: "src/Counter.sol:Counter".parse().unwrap(),
521 block: None,
522 constructor_args: None,
523 encoded_constructor_args: None,
524 constructor_args_path: None,
525 rpc_url: None,
526 network: None,
527 etherscan: EtherscanOpts::default(),
528 verifier: VerifierArgs::default(),
529 root: Some(prj.root().to_path_buf()),
530 ignore: None,
531 };
532
533 let artifact = build_project(&args, &config).unwrap();
534
535 assert!(artifact.bytecode.and_then(|bytecode| bytecode.into_bytes()).is_some());
536 }
537
538 #[test]
539 fn load_fork_config_and_evm_opts_serializes_chain_as_id() {
540 let config = Config { chain: Some(NamedChain::Mainnet.into()), ..Default::default() };
541
542 let (fork_config, evm_opts) = load_fork_config_and_evm_opts(&config).unwrap();
543
544 assert_eq!(fork_config.chain, Some(NamedChain::Mainnet.into()));
545 assert_eq!(evm_opts.env.chain_id, Some(1));
546 }
547
548 #[test]
549 fn test_host_only() {
550 assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
551 assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
552 assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
553 }
554
555 #[test]
556 fn wrap_verifier_url_error_passes_through_when_no_url() {
557 let err = eyre::eyre!("upstream failure");
558 let wrapped = wrap_verifier_url_error(err, None, true);
559 assert_eq!(wrapped.to_string(), "upstream failure");
560 }
561
562 #[test]
563 fn wrap_verifier_url_error_adds_hint_for_host_only_etherscan_url() {
564 let err = eyre::eyre!("upstream failure");
565 let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), true);
566 let msg = format!("{wrapped:#}");
567 assert!(msg.contains("host-only"), "message: {msg}");
568 assert!(msg.contains("--verifier-url https://contracts.tempo.xyz/api"), "message: {msg}");
569 assert!(msg.contains("--verifier sourcify"), "message: {msg}");
570 }
571
572 #[test]
575 fn wrap_verifier_url_error_does_not_hint_for_non_etherscan_provider() {
576 let err = eyre::eyre!("upstream failure");
577 let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), false);
578 assert_eq!(wrapped.to_string(), "upstream failure");
579 }
580
581 #[test]
582 fn wrap_verifier_url_error_reports_invalid_url() {
583 let err = eyre::eyre!("upstream failure");
584 let wrapped = wrap_verifier_url_error(err, Some("not a url"), true);
585 let msg = format!("{wrapped:#}");
586 assert!(msg.contains("Invalid URL"), "message: {msg}");
587 }
588}