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 Graph,
20 artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion},
21 compilers::ParsedSource,
22 multi::{MultiCompilerLanguage, MultiCompilerParser},
23 utils::canonicalize,
24};
25use foundry_config::Config;
26use foundry_evm::{
27 constants::DEFAULT_CREATE2_DEPLOYER,
28 core::{
29 FoundryBlock as _,
30 decode::RevertDecoder,
31 evm::{BlockEnvFor, BlockResponseFor, EvmEnvFor, FoundryEvmNetwork, SpecFor, TxEnvFor},
32 },
33 executors::TracingExecutor,
34 opts::EvmOpts,
35 traces::TraceRequirements,
36 utils::{apply_chain_and_block_specific_env_changes, block_env_from_header},
37};
38use foundry_evm_networks::NetworkConfigs;
39use reqwest::Url;
40use revm::{bytecode::Bytecode, context::Block as _, database::Database};
41use semver::{BuildMetadata, Version};
42use serde::{Deserialize, Serialize};
43use yansi::Paint;
44
45#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
47pub enum BytecodeType {
48 #[serde(rename = "creation")]
49 Creation,
50 #[serde(rename = "runtime")]
51 Runtime,
52}
53
54impl BytecodeType {
55 pub const fn is_creation(&self) -> bool {
57 matches!(self, Self::Creation)
58 }
59
60 pub const fn is_runtime(&self) -> bool {
62 matches!(self, Self::Runtime)
63 }
64}
65
66#[derive(Debug, Serialize, Deserialize)]
67pub struct JsonResult {
68 pub bytecode_type: BytecodeType,
69 pub match_type: Option<VerificationType>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub message: Option<String>,
72}
73
74pub fn match_bytecodes(
75 local_bytecode: &[u8],
76 bytecode: &[u8],
77 constructor_args: &[u8],
78 is_runtime: bool,
79 bytecode_hash: BytecodeHash,
80) -> Option<VerificationType> {
81 if local_bytecode == bytecode {
83 if bytecode_hash == BytecodeHash::None {
86 return Some(VerificationType::Partial);
87 }
88
89 Some(VerificationType::Full)
90 } else {
91 is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
92 .then_some(VerificationType::Partial)
93 }
94}
95
96pub fn build_project(
97 args: &VerifyBytecodeArgs,
98 config: &Config,
99) -> Result<CompactContractBytecode> {
100 let project = config.project()?;
101 let compiler = ProjectCompiler::new().quiet(true);
102
103 let target_path = match args.contract.path() {
104 Some(path) => Some(canonicalize(project.root().join(path))?),
105 None => Graph::<MultiCompilerParser>::resolve(&project.paths).ok().and_then(|graph| {
106 if graph
107 .nodes
108 .iter()
109 .any(|node| matches!(node.data.language(), MultiCompilerLanguage::Vyper(_)))
110 {
111 return None;
112 }
113 let mut matches = graph.nodes.iter().filter(|node| {
114 node.data.contract_names().iter().any(|name| name == &args.contract.name)
115 });
116 let target = matches.next()?;
117 (matches.next().is_none()
118 && graph.input_nodes().any(|input| input.path() == target.path()))
119 .then(|| target.path().to_path_buf())
120 }),
121 };
122 if let Some(target_path) = target_path {
123 let mut output = compiler.files([target_path.clone()]).compile(&project)?;
124 let artifact =
125 find_matching_contract_artifact(&mut output, &target_path, Some(&args.contract.name))?;
126 return Ok(artifact.into_contract_bytecode());
127 }
128
129 let mut output = compiler.compile(&project)?;
130
131 let artifact = output
132 .remove_contract(&args.contract)
133 .ok_or_eyre("Build Error: Contract artifact not found locally")?;
134
135 Ok(artifact.into_contract_bytecode())
136}
137
138pub fn print_result(
139 res: Option<VerificationType>,
140 bytecode_type: BytecodeType,
141 json_results: &mut Vec<JsonResult>,
142 etherscan_metadata: Option<&Metadata>,
143 config: &Config,
144) {
145 if let Some(res) = res {
146 if shell::is_json() {
147 let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
148 json_results.push(json_res);
149 } else {
150 let _ = sh_println!(
151 "{} with status {}",
152 format!("{bytecode_type:?} code matched").green().bold(),
153 res.green().bold()
154 );
155 }
156 } else if !shell::is_json() {
157 let _ = sh_err!(
158 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
159 );
160 if let Some(etherscan_metadata) = etherscan_metadata {
161 let mismatches = find_mismatch_in_settings(etherscan_metadata, config);
162 for mismatch in mismatches {
163 let _ = sh_eprintln!("{}", mismatch.red().bold());
164 }
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 if constructor_args.is_empty() || is_runtime {
186 return try_extract_and_compare_bytecode(local_bytecode, bytecode);
188 }
189
190 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 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.map_or("unknown".to_string(), |runs| runs.to_string()),
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 Err(EtherscanError::EmptyResult { status, message })
248 if status == "1" && message == "OK" =>
249 {
250 maybe_predeploy = true;
251 Ok((None, maybe_predeploy))
252 }
253 Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
255 maybe_predeploy = true;
256 Ok((None, maybe_predeploy))
257 }
258 Err(e) => {
259 eyre::bail!("Error fetching creation data from verifier-url: {:?}", e);
260 }
261 }
262}
263
264pub fn check_and_encode_args(
265 artifact: &CompactContractBytecode,
266 args: Vec<String>,
267) -> Result<Vec<u8>, eyre::ErrReport> {
268 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
269 if constructor.inputs.len() != args.len() {
270 eyre::bail!(
271 "Mismatch of constructor arguments length. Expected {}, got {}",
272 constructor.inputs.len(),
273 args.len()
274 );
275 }
276 encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode())
277 } else {
278 Ok(Vec::new())
279 }
280}
281
282pub fn check_explorer_args(source_code: &ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
283 if let Some(args) = source_code.items.first() {
284 Ok(args.constructor_arguments.clone())
285 } else {
286 eyre::bail!("No constructor arguments found from block explorer");
287 }
288}
289
290pub fn check_args_len(
291 artifact: &CompactContractBytecode,
292 args: &Bytes,
293) -> Result<(), eyre::ErrReport> {
294 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
295 && !constructor.inputs.is_empty()
296 && args.is_empty()
297 {
298 eyre::bail!(
299 "Contract expects {} constructor argument(s), but none were provided",
300 constructor.inputs.len()
301 );
302 }
303 Ok(())
304}
305
306pub fn load_fork_config_and_evm_opts(config: &Config) -> Result<(Config, EvmOpts)> {
307 let chain = config.chain;
308 let mut fork_config = config.clone();
309 fork_config.chain = None;
310
311 let (mut fork_config, mut evm_opts) = fork_config.load_config_and_evm_opts()?;
312 fork_config.chain = chain;
313 if let Some(chain) = chain {
314 evm_opts.env.chain_id = Some(chain.id());
315 }
316
317 Ok((fork_config, evm_opts))
318}
319
320pub async fn get_tracing_executor<FEN>(
321 fork_config: &mut Config,
322 fork_blk_num: u64,
323 evm_version: EvmVersion,
324 evm_opts: EvmOpts,
325) -> Result<(EvmEnvFor<FEN>, TxEnvFor<FEN>, TracingExecutor<FEN>)>
326where
327 FEN: FoundryEvmNetwork,
328{
329 fork_config.fork_block_number = Some(fork_blk_num);
330 fork_config.evm_version = evm_version;
331
332 let create2_deployer = evm_opts.create2_deployer;
333 let (evm_env, tx_env, fork, _chain, networks) =
334 TracingExecutor::<FEN>::get_fork_material(fork_config, evm_opts).await?;
335
336 let executor = TracingExecutor::<FEN>::new(
337 (evm_env.clone(), tx_env.clone()),
338 fork,
339 Some(fork_config.evm_version),
340 TraceRequirements::none().with_calls(true),
341 networks,
342 create2_deployer,
343 None,
344 )?;
345
346 Ok((evm_env, tx_env, executor))
347}
348
349pub fn configure_env_block<FEN>(
350 evm_env: &mut EvmEnvFor<FEN>,
351 block: &BlockResponseFor<FEN>,
352 config: NetworkConfigs,
353) where
354 FEN: FoundryEvmNetwork,
355{
356 let number = evm_env.block_env.number();
357 evm_env.block_env = block_env_from_header::<BlockEnvFor<FEN>>(block.header());
358 evm_env.block_env.set_number(number);
359 apply_chain_and_block_specific_env_changes::<FEN::Network, _, _>(evm_env, block, config);
360}
361
362pub fn deploy_contract<FEN>(
363 executor: &mut TracingExecutor<FEN>,
364 evm_env: &EvmEnvFor<FEN>,
365 tx_env: &TxEnvFor<FEN>,
366 spec_id: SpecFor<FEN>,
367 to: TxKind,
368) -> Result<Address, eyre::ErrReport>
369where
370 FEN: FoundryEvmNetwork,
371{
372 let mut evm_env = evm_env.clone();
373 evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec_id);
374
375 if let TxKind::Call(to) = to {
376 if to != DEFAULT_CREATE2_DEPLOYER {
377 eyre::bail!(
378 "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
379 );
380 }
381 let result = executor.transact_with_env(evm_env, tx_env.clone())?;
382
383 trace!(transact_result = ?result.exit_reason);
384
385 if result.reverted {
386 let decoded_reason = if result.result.is_empty() {
387 String::new()
388 } else {
389 format!(": {}", RevertDecoder::default().decode(&result.result, result.exit_reason))
390 };
391 eyre::bail!(
392 "Failed to deploy contract via CREATE2 on fork at block{decoded_reason}.\n\
393 This typically happens when your local bytecode differs from what was actually deployed.\n\
394 Common causes:\n\
395 - Your contract source is not at the same commit used during deployment\n\
396 - Cached build artifacts are stale (try `forge clean && forge build`)\n\
397 - Compiler settings (optimizer, evm_version, via_ir) don't match the deployment"
398 );
399 }
400
401 if result.result.len() != 20 {
402 eyre::bail!(
403 "Failed to deploy contract via CREATE2 on fork at block: deployer returned {} bytes instead of 20.\n\
404 This may indicate a bytecode mismatch - ensure your source code matches the deployed contract.",
405 result.result.len()
406 );
407 }
408
409 Ok(Address::from_slice(&result.result))
410 } else {
411 let deploy_result = executor.deploy_with_env(evm_env, tx_env.clone(), None)?;
412 trace!(deploy_result = ?deploy_result.raw.exit_reason);
413 Ok(deploy_result.address)
414 }
415}
416
417pub async fn get_runtime_codes<FEN>(
418 executor: &mut TracingExecutor<FEN>,
419 provider: &impl Provider<FEN::Network>,
420 address: Address,
421 fork_address: Address,
422 block: Option<u64>,
423) -> Result<(Bytecode, Bytes)>
424where
425 FEN: FoundryEvmNetwork,
426{
427 let fork_runtime_code = executor
428 .backend_mut()
429 .basic(fork_address)?
430 .ok_or_else(|| {
431 eyre::eyre!(
432 "Failed to get runtime code for contract deployed on fork at address {}",
433 fork_address
434 )
435 })?
436 .code
437 .ok_or_else(|| {
438 eyre::eyre!(
439 "Bytecode does not exist for contract deployed on fork at address {}",
440 fork_address
441 )
442 })?;
443
444 let block_id = block.map_or_else(BlockId::latest, BlockId::number);
445 let onchain_runtime_code = provider.get_code_at(address).block_id(block_id).await?;
446
447 Ok((fork_runtime_code, onchain_runtime_code))
448}
449
450pub fn is_host_only(url: &Url) -> bool {
454 matches!(url.path(), "/" | "")
455}
456
457pub fn wrap_verifier_url_error(
464 err: eyre::Error,
465 verifier_url: Option<&str>,
466 using_etherscan: bool,
467) -> eyre::Error {
468 let Some(verifier_url) = verifier_url else { return err };
469 let url = match Url::parse(verifier_url) {
470 Ok(mut url) => {
471 let _ = url.set_username("");
472 let _ = url.set_password(None);
473 url.set_query(None);
474 url.set_fragment(None);
475 url
476 }
477 Err(url_err) => {
478 return err.wrap_err(format!("Invalid verifier URL provided: {url_err}"));
479 }
480 };
481 if is_host_only(&url) && using_etherscan {
482 return err.wrap_err(format!(
483 "Verifier `etherscan` requires an API endpoint, but `--verifier-url` is host-only: `{url}`.\n\
484 Fixes (pick one):\n\
485 - Append the API path, e.g. `--verifier-url {url}api`\n\
486 - Switch verifier, e.g. `--verifier sourcify` (works with host-only URLs)"
487 ));
488 }
489 err
490}
491
492pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
503 if version.build == BuildMetadata::EMPTY {
504 Ok(lookup_compiler_version(&version).await?)
505 } else {
506 Ok(version)
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::verify::VerifierArgs;
514 use foundry_cli::opts::EtherscanOpts;
515 use foundry_compilers::PathStyle;
516 use foundry_config::NamedChain;
517 use foundry_test_utils::TestProject;
518
519 #[test]
520 fn build_project_finds_artifact_by_relative_contract_path() {
521 let prj = TestProject::new("verify-bytecode-relative-path", PathStyle::Dapptools);
522 prj.add_source(
523 "Counter.sol",
524 r#"
525pragma solidity 0.8.16;
526
527contract Counter {
528 uint256 public number;
529}
530"#,
531 );
532 prj.add_source(
533 "Broken.sol",
534 r#"
535pragma solidity 0.8.16;
536
537contract Broken {
538 this is not valid Solidity
539}
540"#,
541 );
542
543 let mut config = Config::load_with_root(prj.root()).unwrap();
544 config.solc = Some("0.8.16".into());
545 let args = VerifyBytecodeArgs {
546 address: Address::ZERO,
547 contract: "src/Counter.sol:Counter".parse().unwrap(),
548 block: None,
549 constructor_args: None,
550 encoded_constructor_args: None,
551 constructor_args_path: None,
552 rpc_url: None,
553 network: None,
554 etherscan: EtherscanOpts::default(),
555 verifier: VerifierArgs::default(),
556 libraries: Vec::new(),
557 root: Some(prj.root().to_path_buf()),
558 ignore: None,
559 };
560
561 let artifact = build_project(&args, &config).unwrap();
562
563 assert!(artifact.bytecode.and_then(|bytecode| bytecode.into_bytes()).is_some());
564 }
565
566 #[test]
567 fn load_fork_config_and_evm_opts_serializes_chain_as_id() {
568 let config = Config { chain: Some(NamedChain::Mainnet.into()), ..Default::default() };
569
570 let (fork_config, evm_opts) = load_fork_config_and_evm_opts(&config).unwrap();
571
572 assert_eq!(fork_config.chain, Some(NamedChain::Mainnet.into()));
573 assert_eq!(evm_opts.env.chain_id, Some(1));
574 }
575
576 #[test]
577 fn test_host_only() {
578 assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
579 assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
580 assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
581 }
582
583 #[test]
584 fn wrap_verifier_url_error_passes_through_when_no_url() {
585 let err = eyre::eyre!("upstream failure");
586 let wrapped = wrap_verifier_url_error(err, None, true);
587 assert_eq!(wrapped.to_string(), "upstream failure");
588 }
589
590 #[test]
591 fn wrap_verifier_url_error_adds_hint_for_host_only_etherscan_url() {
592 let err = eyre::eyre!("upstream failure");
593 let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), true);
594 let msg = format!("{wrapped:#}");
595 assert!(msg.contains("host-only"), "message: {msg}");
596 assert!(msg.contains("--verifier-url https://contracts.tempo.xyz/api"), "message: {msg}");
597 assert!(msg.contains("--verifier sourcify"), "message: {msg}");
598 }
599
600 #[test]
603 fn wrap_verifier_url_error_does_not_hint_for_non_etherscan_provider() {
604 let err = eyre::eyre!("upstream failure");
605 let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), false);
606 assert_eq!(wrapped.to_string(), "upstream failure");
607 }
608
609 #[test]
610 fn wrap_verifier_url_error_reports_invalid_url() {
611 let err = eyre::eyre!("upstream failure");
612 let wrapped = wrap_verifier_url_error(err, Some("not a url"), true);
613 let msg = format!("{wrapped:#}");
614 assert!(msg.contains("Invalid verifier URL"), "message: {msg}");
615 assert!(!msg.contains("not a url"), "message: {msg}");
616 }
617
618 #[test]
619 fn wrap_verifier_url_error_redacts_credentials_and_query() {
620 let err = eyre::eyre!("upstream failure");
621 let wrapped = wrap_verifier_url_error(
622 err,
623 Some("https://user:secret@example.com?api_key=secret"),
624 true,
625 );
626 let msg = format!("{wrapped:#}");
627 assert!(msg.contains("https://example.com/"));
628 assert!(!msg.contains("user"));
629 assert!(!msg.contains("secret"));
630 assert!(!msg.contains("api_key"));
631 }
632}