1use crate::{
4 RetryArgs,
5 etherscan::EtherscanVerificationProvider,
6 provider::{
7 ExternalVerificationContext, VerificationContext, VerificationProvider,
8 VerificationProviderType,
9 },
10 sourcify::SourcifyVerificationProvider,
11 utils::wrap_verifier_url_error,
12};
13use alloy_primitives::{Address, TxHash, map::HashSet};
14use alloy_provider::Provider;
15use clap::{Parser, ValueEnum, ValueHint};
16use eyre::{Context, Result};
17use foundry_cli::{
18 opts::{EtherscanOpts, RpcOpts},
19 utils::{self, LoadConfig},
20};
21use foundry_common::{ContractsByArtifact, compile::ProjectCompiler};
22use foundry_compilers::{artifacts::EvmVersion, info::ContractInfo};
23use foundry_config::{
24 Chain, Config,
25 figment::{
26 Error, Metadata, Profile, Provider as FigmentProvider,
27 value::{Dict, Map, Value},
28 },
29 impl_figment_convert, impl_figment_convert_cast,
30};
31use itertools::Itertools;
32use reqwest::{Client, StatusCode, Url};
33use semver::BuildMetadata;
34use serde::Deserialize;
35use std::{path::PathBuf, time::Duration};
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38enum VerifierCredentialProbe {
39 Accepted,
40 InvalidApiKey,
41 Inconclusive,
42}
43
44#[derive(Debug, Deserialize)]
45struct EtherscanProbeResponse {
46 status: String,
47 result: Option<serde_json::Value>,
48}
49
50fn verifier_credential_probe_query(api_key: Option<&str>) -> Vec<(&'static str, String)> {
51 let mut query = vec![
52 ("module", "contract".to_string()),
53 ("action", "getabi".to_string()),
54 ("address", Address::ZERO.to_string()),
55 ];
56 if let Some(api_key) = api_key {
57 query.push(("apikey", api_key.to_string()));
58 }
59 query
60}
61
62fn classify_verifier_credential_response(
63 status: StatusCode,
64 body: &str,
65) -> VerifierCredentialProbe {
66 let lower = body.to_lowercase();
67 if lower.contains("invalid api key") || lower.contains("invalid_api_key") {
68 return VerifierCredentialProbe::InvalidApiKey;
69 }
70
71 if lower.contains("contract source code not verified")
72 || lower.contains("contract not found")
73 || lower.contains("contract was not found")
74 {
75 return VerifierCredentialProbe::Accepted;
76 }
77
78 if status == StatusCode::UNAUTHORIZED {
79 return VerifierCredentialProbe::InvalidApiKey;
80 }
81
82 if !status.is_success()
83 || lower.contains("max rate limit reached")
84 || lower.contains("sorry, you have been blocked")
85 || lower.contains("checking if the site connection is secure")
86 {
87 return VerifierCredentialProbe::Inconclusive;
88 }
89
90 match serde_json::from_str::<EtherscanProbeResponse>(body) {
91 Ok(resp) if resp.status == "1" => VerifierCredentialProbe::Accepted,
92 Ok(resp) => resp
93 .result
94 .and_then(|result| result.as_str().map(str::to_lowercase))
95 .map(|result| {
96 if result.contains("invalid api key") || result.contains("invalid_api_key") {
97 VerifierCredentialProbe::InvalidApiKey
98 } else if result.contains("max rate limit reached") {
99 VerifierCredentialProbe::Inconclusive
100 } else if result.contains("contract source code not verified")
101 || result.contains("contract not found")
102 || result.contains("contract was not found")
103 {
104 VerifierCredentialProbe::Accepted
105 } else {
106 VerifierCredentialProbe::Inconclusive
107 }
108 })
109 .unwrap_or(VerifierCredentialProbe::Inconclusive),
110 Err(_) => VerifierCredentialProbe::Inconclusive,
111 }
112}
113
114fn parse_http_verifier_url(url: &str, label: &str) -> Result<Url> {
115 let url = Url::parse(url).wrap_err_with(|| format!("invalid {label} URL `{url}`"))?;
116 if !matches!(url.scheme(), "http" | "https") {
117 eyre::bail!("invalid {label} URL `{url}`: URL scheme must be http or https");
118 }
119 Ok(url)
120}
121
122async fn probe_verifier_credentials(
123 url: Url,
124 api_key: Option<&str>,
125) -> Result<VerifierCredentialProbe, reqwest::Error> {
126 let resp = Client::new()
127 .get(url)
128 .query(&verifier_credential_probe_query(api_key))
129 .timeout(Duration::from_secs(10))
130 .send()
131 .await?;
132 let status = resp.status();
133 let body = resp.text().await?;
134 Ok(classify_verifier_credential_response(status, &body))
135}
136
137#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
141pub enum ContractLanguage {
142 Solidity,
144 Vyper,
146}
147
148#[derive(Clone, Debug, Default, Parser)]
150pub struct VerifierArgs {
151 #[arg(long, help_heading = "Verifier options", value_enum)]
153 pub verifier: Option<VerificationProviderType>,
154
155 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_KEY")]
157 pub verifier_api_key: Option<String>,
158
159 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")]
161 pub verifier_url: Option<String>,
162}
163
164impl VerifierArgs {
165 pub fn effective_type(&self) -> VerificationProviderType {
170 self.verifier.unwrap_or_default()
171 }
172
173 pub const fn is_explicitly_set(&self) -> bool {
175 self.verifier.is_some()
176 }
177
178 pub fn resolve_api_key<'a>(&'a self, etherscan_key: Option<&'a str>) -> Option<&'a str> {
181 self.verifier_api_key.as_deref().or(etherscan_key)
182 }
183
184 pub async fn check_credentials(
189 &self,
190 api_key: Option<&str>,
191 chain: Chain,
192 config: &Config,
193 ) -> eyre::Result<()> {
194 let resolved = self.resolve(api_key, Some(chain));
195 match resolved {
196 VerificationProviderType::Etherscan
197 | VerificationProviderType::Blockscout
198 | VerificationProviderType::Oklink => {
199 let etherscan_opts =
200 EtherscanOpts { key: api_key.map(str::to_owned), chain: Some(chain) };
201 let client = EtherscanVerificationProvider::default().client(
202 ðerscan_opts,
203 self,
204 config,
205 )?;
206 match tokio::time::timeout(
207 Duration::from_secs(10),
208 probe_verifier_credentials(
209 client.etherscan_api_url().clone(),
210 client.api_key(),
211 ),
212 )
213 .await
214 {
215 Err(_) => {
216 sh_warn!("verifier credential check timed out, proceeding anyway")?;
217 }
218 Ok(Ok(VerifierCredentialProbe::Accepted)) => {}
219 Ok(Ok(VerifierCredentialProbe::InvalidApiKey)) => {
220 eyre::bail!("verifier credential check failed: invalid API key");
221 }
222 Ok(Ok(VerifierCredentialProbe::Inconclusive) | Err(_)) => {
223 sh_warn!("verifier credential check inconclusive, proceeding anyway")?;
224 }
225 }
226 }
227 VerificationProviderType::Custom => {
228 if let Some(url) = &self.verifier_url {
231 let url = parse_http_verifier_url(url, "verifier")?;
232 match probe_verifier_credentials(url, api_key).await {
233 Err(_) => {
234 sh_warn!("verifier credential check failed, proceeding anyway")?;
235 }
236 Ok(
237 VerifierCredentialProbe::Accepted
238 | VerifierCredentialProbe::Inconclusive,
239 ) => {}
240 Ok(VerifierCredentialProbe::InvalidApiKey) => {
241 eyre::bail!("verifier credential check failed: invalid API key");
242 }
243 }
244 }
245 }
246 VerificationProviderType::Sourcify => {
247 if let Some(url) = &self.verifier_url {
249 let url = parse_http_verifier_url(url, "Sourcify")?;
250 match Client::new()
251 .get(url.clone())
252 .timeout(Duration::from_secs(10))
253 .send()
254 .await
255 {
256 Err(_) => {
257 sh_warn!(
258 "Sourcify URL `{url}` could not be reached, proceeding anyway"
259 )?;
260 }
261 Ok(resp) => {
262 let status = resp.status();
263 if !status.is_success() && status != StatusCode::NOT_FOUND {
264 sh_warn!(
265 "Sourcify URL `{url}` returned HTTP {status}, proceeding anyway"
266 )?;
267 }
268 }
269 }
270 }
271 }
272 }
273 Ok(())
274 }
275
276 pub fn resolve(
286 &self,
287 etherscan_key: Option<&str>,
288 chain: Option<Chain>,
289 ) -> VerificationProviderType {
290 if let Some(v) = self.verifier {
291 return v;
292 }
293 let has_key = etherscan_key.is_some_and(|k| !k.is_empty());
294 if has_key && !chain.is_some_and(|c| c.is_custom_sourcify()) {
298 let chain_has_etherscan_url = chain.is_none_or(|c| c.etherscan_urls().is_some());
299 if chain_has_etherscan_url || self.verifier_url.is_some() {
300 return VerificationProviderType::Etherscan;
301 }
302 }
303 VerificationProviderType::Sourcify
304 }
305}
306
307#[derive(Clone, Debug, Parser)]
309pub struct VerifyArgs {
310 pub address: Address,
312
313 pub contract: Option<ContractInfo>,
315
316 #[arg(
318 long,
319 conflicts_with = "constructor_args_path",
320 value_name = "ARGS",
321 visible_alias = "encoded-constructor-args"
322 )]
323 pub constructor_args: Option<String>,
324
325 #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
327 pub constructor_args_path: Option<PathBuf>,
328
329 #[arg(long)]
331 pub guess_constructor_args: bool,
332
333 #[arg(long)]
335 pub creation_transaction_hash: Option<TxHash>,
336
337 #[arg(long, value_name = "VERSION")]
339 pub compiler_version: Option<String>,
340
341 #[arg(long, value_name = "PROFILE_NAME")]
343 pub compilation_profile: Option<String>,
344
345 #[arg(long, visible_alias = "optimizer-runs", value_name = "NUM")]
347 pub num_of_optimizations: Option<usize>,
348
349 #[arg(long)]
351 pub flatten: bool,
352
353 #[arg(short, long)]
355 pub force: bool,
356
357 #[arg(long)]
359 pub skip_is_verified_check: bool,
360
361 #[arg(long)]
363 pub watch: bool,
364
365 #[arg(skip)]
368 pub print_submission_result_to_stdout: bool,
369
370 #[arg(long, help_heading = "Linker options", env = "DAPP_LIBRARIES")]
372 pub libraries: Vec<String>,
373
374 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
379 pub root: Option<PathBuf>,
380
381 #[arg(long, conflicts_with = "flatten")]
386 pub show_standard_json_input: bool,
387
388 #[arg(long)]
390 pub via_ir: bool,
391
392 #[arg(
400 long,
401 value_name = "LICENSE",
402 help_heading = "Verifier options",
403 value_parser = parse_etherscan_license_type,
404 )]
405 pub license_type: Option<String>,
406
407 #[arg(long)]
411 pub evm_version: Option<EvmVersion>,
412
413 #[arg(long, help_heading = "Compiler options")]
415 pub no_auto_detect: bool,
416
417 #[arg(long = "use", help_heading = "Compiler options", value_name = "SOLC_VERSION")]
421 pub use_solc: Option<String>,
422
423 #[command(flatten)]
424 pub etherscan: EtherscanOpts,
425
426 #[command(flatten)]
427 pub rpc: RpcOpts,
428
429 #[command(flatten)]
430 pub retry: RetryArgs,
431
432 #[command(flatten)]
433 pub verifier: VerifierArgs,
434
435 #[arg(long, value_enum)]
439 pub language: Option<ContractLanguage>,
440}
441
442pub fn parse_etherscan_license_type(value: &str) -> Result<String, String> {
443 let value = value.trim();
444 if value.is_empty() {
445 return Err("license type cannot be empty".into());
446 }
447
448 if let Ok(code) = value.parse::<u32>() {
449 return Ok(code.to_string());
450 }
451
452 let normalized = normalize_license_type(value);
453 let code = match normalized.as_str() {
454 "none" | "no-license" | "unlicensed" => 1,
455 "unlicense" | "the-unlicense" => 2,
456 "mit" | "mit-license" => 3,
457 "gpl-2.0" | "gpl-2.0+" | "gpl-2.0-only" | "gpl-2.0-or-later" | "gplv2" | "gnu-gplv2" => 4,
458 "gpl-3.0" | "gpl-3.0+" | "gpl-3.0-only" | "gpl-3.0-or-later" | "gplv3" | "gnu-gplv3" => 5,
459 "lgpl-2.1" | "lgpl-2.1+" | "lgpl-2.1-only" | "lgpl-2.1-or-later" | "lgplv2.1"
460 | "gnu-lgplv2.1" => 6,
461 "lgpl-3.0" | "lgpl-3.0+" | "lgpl-3.0-only" | "lgpl-3.0-or-later" | "lgplv3"
462 | "gnu-lgplv3" => 7,
463 "bsd-2-clause" => 8,
464 "bsd-3-clause" => 9,
465 "mpl-2.0" => 10,
466 "osl-3.0" => 11,
467 "apache-2.0" | "apache-license-2.0" => 12,
468 "agpl-3.0" | "agpl-3.0+" | "agpl-3.0-only" | "agpl-3.0-or-later" | "agplv3"
469 | "gnu-agplv3" => 13,
470 "bsl-1.1" | "busl-1.1" | "business-source-license-1.1" => 14,
471 _ => {
472 return Err(format!(
473 "unsupported Etherscan license type `{value}`; expected a numeric code or a \
474 supported SPDX identifier such as MIT, Apache-2.0, GPL-3.0-or-later, or \
475 AGPL-3.0-or-later"
476 ));
477 }
478 };
479
480 Ok(code.to_string())
481}
482
483fn normalize_license_type(value: &str) -> String {
484 let mut normalized = String::with_capacity(value.len());
485 let mut last_was_dash = false;
486
487 for ch in value.trim().chars() {
488 let ch = match ch {
489 '_' | ' ' | '\t' | '\n' | '\r' => '-',
490 _ => ch.to_ascii_lowercase(),
491 };
492
493 if ch == '-' {
494 if !last_was_dash {
495 normalized.push(ch);
496 }
497 last_was_dash = true;
498 } else {
499 normalized.push(ch);
500 last_was_dash = false;
501 }
502 }
503
504 normalized.trim_matches('-').to_string()
505}
506
507impl_figment_convert!(VerifyArgs);
508
509impl FigmentProvider for VerifyArgs {
510 fn metadata(&self) -> Metadata {
511 Metadata::named("Verify Provider")
512 }
513
514 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
515 let mut dict = self.etherscan.dict();
516 dict.extend(self.rpc.dict());
517
518 if let Some(root) = self.root.as_ref() {
519 dict.insert("root".to_string(), Value::serialize(root)?);
520 }
521 if let Some(optimizer_runs) = self.num_of_optimizations {
522 dict.insert("optimizer".to_string(), Value::serialize(true)?);
523 dict.insert("optimizer_runs".to_string(), Value::serialize(optimizer_runs)?);
524 }
525 if let Some(evm_version) = self.evm_version {
526 dict.insert("evm_version".to_string(), Value::serialize(evm_version)?);
527 }
528 if self.via_ir {
529 dict.insert("via_ir".to_string(), Value::serialize(self.via_ir)?);
530 }
531
532 if self.no_auto_detect {
533 dict.insert("auto_detect_solc".to_string(), Value::serialize(false)?);
534 }
535
536 if let Some(ref solc) = self.use_solc {
537 let solc = solc.trim_start_matches("solc:");
538 dict.insert("solc".to_string(), Value::serialize(solc)?);
539 }
540
541 if let Some(api_key) = &self.verifier.verifier_api_key {
542 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
543 }
544
545 Ok(Map::from([(Config::selected_profile(), dict)]))
546 }
547}
548
549struct ProviderRun {
550 label: VerificationProviderType,
551 args: VerifyArgs,
552 provider: Box<dyn VerificationProvider>,
553 required: bool,
555}
556
557#[derive(Clone)]
558enum RunContext {
559 Local(Box<VerificationContext>),
560 External(Box<ExternalVerificationContext>),
561}
562
563impl VerifyArgs {
564 pub async fn run(self) -> Result<()> {
566 let context = self.resolve_context().await?;
567 let config = context.config.clone();
568 self.run_with_resolved_context(RunContext::Local(Box::new(context)), config).await
569 }
570
571 pub async fn run_with_external_context(
573 self,
574 context: ExternalVerificationContext,
575 ) -> Result<()> {
576 self.validate_external_args()?;
577 eyre::ensure!(
578 !context.target.is_empty()
579 && context.target.len() <= 8193
580 && context.target.is_ascii()
581 && !context.target.bytes().any(|byte| byte.is_ascii_control()),
582 "external contract identifier must contain bounded printable ASCII"
583 );
584 let config = context.config.clone();
585 self.run_with_resolved_context(RunContext::External(Box::new(context)), config).await
586 }
587
588 fn validate_external_args(&self) -> Result<()> {
589 eyre::ensure!(!self.flatten, "flattening is unsupported for external verification");
590 eyre::ensure!(
591 !matches!(self.language, Some(ContractLanguage::Vyper)),
592 "Vyper is unsupported for external verification"
593 );
594 eyre::ensure!(
595 !self.guess_constructor_args,
596 "constructor argument guessing is unsupported for external verification"
597 );
598 eyre::ensure!(
599 self.constructor_args_path.is_none(),
600 "constructor args paths are unsupported for external verification; provide encoded constructor args"
601 );
602 Ok(())
603 }
604
605 async fn run_with_resolved_context(
606 mut self,
607 context: RunContext,
608 config: Config,
609 ) -> Result<()> {
610 if self.guess_constructor_args && config.get_rpc_url().is_none() {
611 eyre::bail!(
612 "You have to provide a valid RPC URL to use --guess-constructor-args feature"
613 );
614 }
615
616 let chain = match config.get_rpc_url() {
617 Some(_) => {
618 let provider = utils::get_provider(&config)?;
619 utils::get_chain(config.chain, provider).await?
620 }
621 None => config.chain.unwrap_or_default(),
622 };
623
624 self.etherscan.chain = Some(chain);
626 let config_key = match config.get_etherscan_config_with_chain(Some(chain)) {
631 Ok(config) => config.map(|config| config.key),
632 Err(_)
635 if self.verifier.verifier_url.is_some()
636 || self.verifier.verifier.is_some_and(|provider| provider.is_sourcify()) =>
637 {
638 None
639 }
640 Err(err) => return Err(err.into()),
641 }
642 .or_else(|| config.etherscan_api_key.clone());
643 self.etherscan.key =
644 self.verifier.resolve_api_key(config_key.as_deref()).map(str::to_owned);
645
646 let had_user_verifier_url = self.verifier.verifier_url.is_some();
650
651 let etherscan_key = self.etherscan.key();
655 let resolved = self.verifier.resolve(etherscan_key.as_deref(), self.etherscan.chain);
656
657 if resolved.is_sourcify()
660 && !had_user_verifier_url
661 && let Some(url) = sourcify_api_url(chain)
662 {
663 self.verifier.verifier_url = Some(url);
664 }
665
666 if self.show_standard_json_input {
667 match &context {
668 RunContext::Local(context) => {
669 let args = EtherscanVerificationProvider::default()
670 .create_verify_request(&self, context)
671 .await?;
672 sh_println!("{}", args.source)?;
673 }
674 RunContext::External(context) => {
675 sh_println!("{}", serde_json::to_string(&context.standard_json_input)?)?;
676 }
677 }
678 return Ok(());
679 }
680
681 let verifier_url = self.verifier.verifier_url.clone();
682 sh_status!("Start verifying contract `{}` deployed on {chain}", self.address)?;
683 if let Some(version) = &self.evm_version {
684 sh_status!("EVM version: {version}")?;
685 }
686 if let Some(version) = &self.compiler_version {
687 sh_status!("Compiler version: {version}")?;
688 }
689 if let Some(optimizations) = &self.num_of_optimizations {
690 sh_status!("Optimizations: {optimizations}")?
691 }
692 if let Some(args) = &self.constructor_args
693 && !args.is_empty()
694 {
695 sh_status!("Constructor args: {args}")?
696 }
697 let using_etherscan = resolved.is_etherscan();
698 let runs =
699 self.collect_runs(chain, etherscan_key.as_deref(), resolved, had_user_verifier_url)?;
700
701 let mut required_err = None;
703 let mut pending_check = None;
704 for ProviderRun { label, args, mut provider, required } in runs {
705 sh_status!("\nVerifying on {label}...")?;
706 let watch = args.watch;
707 let submission = match context.clone() {
708 RunContext::Local(context) => provider.submit(args, *context).await,
709 RunContext::External(context) => provider.submit_external(args, *context).await,
710 };
711 match submission {
712 Ok(check_args) => {
713 if required
714 && watch
715 && let Some(check_args) = check_args
716 {
717 pending_check = Some((label, provider, check_args));
718 }
719 }
720 Err(err) => {
721 if required {
722 required_err = Some(wrap_verifier_url_error(
723 err,
724 verifier_url.as_deref(),
725 using_etherscan,
726 ));
727 } else {
728 sh_warn!("{label} verification failed: {err}")?;
729 }
730 }
731 }
732 }
733
734 if required_err.is_none()
736 && let Some((label, provider, check_args)) = pending_check
737 {
738 sh_status!("\nWaiting for {label} verification result...")?;
739 if let Err(err) = provider.check(check_args).await {
740 required_err =
741 Some(wrap_verifier_url_error(err, verifier_url.as_deref(), using_etherscan));
742 }
743 }
744
745 required_err.map_or(Ok(()), Err)
746 }
747
748 fn collect_runs(
753 &self,
754 chain: Chain,
755 etherscan_key: Option<&str>,
756 resolved: VerificationProviderType,
757 had_user_verifier_url: bool,
758 ) -> Result<Vec<ProviderRun>> {
759 let mut runs = Vec::new();
760
761 let primary_provider = resolved.client(
762 etherscan_key,
763 self.etherscan.chain,
764 had_user_verifier_url,
765 self.verifier.is_explicitly_set(),
766 )?;
767 let primary_is_sourcify = resolved.is_sourcify();
768 runs.push(ProviderRun {
769 label: resolved,
770 args: self.clone(),
771 provider: primary_provider,
772 required: true,
773 });
774
775 let is_private_setup = had_user_verifier_url || resolved.is_custom() || is_dev_chain(chain);
779 if !primary_is_sourcify && !is_private_setup {
780 let mut args = self.clone();
781 args.verifier.verifier = Some(VerificationProviderType::Sourcify);
782 args.verifier.verifier_api_key = None;
783 args.verifier.verifier_url = sourcify_api_url(chain);
786 args.watch = false;
787 runs.push(ProviderRun {
788 label: VerificationProviderType::Sourcify,
789 args,
790 provider: Box::<SourcifyVerificationProvider>::default(),
791 required: false,
792 });
793 }
794
795 Ok(runs)
796 }
797
798 pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
800 self.verifier.effective_type().client(
801 self.etherscan.key().as_deref(),
802 self.etherscan.chain,
803 self.verifier.verifier_url.is_some(),
804 self.verifier.is_explicitly_set(),
805 )
806 }
807
808 pub async fn resolve_context(&self) -> Result<VerificationContext> {
811 let mut config = self.load_config_with_dependencies()?;
812 config.libraries.extend(self.libraries.clone());
813
814 let project = config.project()?;
815
816 if let Some(ref contract) = self.contract {
817 let contract_path = if let Some(ref path) = contract.path {
818 project.root().join(PathBuf::from(path))
819 } else {
820 project.find_contract_path(&contract.name)?
821 };
822
823 let cache = project.read_cache_file().ok();
824
825 let mut version = if let Some(ref version) = self.compiler_version {
826 version.trim_start_matches('v').parse()?
827 } else if let Some(ref solc) = config.solc {
828 solc.try_version()?
829 } else if let Some(entry) =
830 cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
831 {
832 let unique_versions = entry
833 .artifacts
834 .get(&contract.name)
835 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
836 .unwrap_or_default();
837
838 if unique_versions.is_empty() {
839 eyre::bail!(
840 "No matching artifact found for {}. This could be due to:\n\
841 - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
842 contract.name
843 );
844 } else if unique_versions.len() > 1 {
845 warn!(
846 "Ambiguous compiler versions found in cache: {}",
847 unique_versions.iter().join(", ")
848 );
849 eyre::bail!(
850 "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
851 );
852 }
853
854 unique_versions.into_iter().next().unwrap().to_owned()
855 } else {
856 eyre::bail!(
857 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
858 );
859 };
860
861 let settings = if let Some(profile) = &self.compilation_profile {
862 if profile == "default" {
863 &project.settings
864 } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
865 settings
866 } else {
867 eyre::bail!("Unknown compilation profile: {}", profile);
868 }
869 } else if let Some((cache, entry)) = cache
870 .as_ref()
871 .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
872 {
873 let profiles = entry
874 .artifacts
875 .get(&contract.name)
876 .and_then(|artifacts| {
877 let mut cached_artifacts = artifacts.get(&version);
878 if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
886 version.build = BuildMetadata::EMPTY;
887 cached_artifacts = artifacts.get(&version);
888 }
889 cached_artifacts
890 })
891 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
892 .unwrap_or_default();
893
894 if profiles.is_empty() {
895 eyre::bail!(
896 "No matching artifact found for {} with compiler version {}. This could be due to:\n\
897 - Compiler version mismatch - the contract was compiled with a different Solidity version",
898 contract.name,
899 version
900 );
901 } else if profiles.len() > 1 {
902 eyre::bail!(
903 "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
904 profiles.iter().join(", ")
905 );
906 }
907
908 let profile = profiles.into_iter().next().unwrap().to_owned();
909 cache.profiles.get(&profile).expect("must be present")
910 } else if project.additional_settings.is_empty() {
911 &project.settings
912 } else {
913 eyre::bail!(
914 "If cache is disabled, compilation profile must be provided with `--compilation-profile` option or set in foundry.toml"
915 );
916 };
917
918 VerificationContext::new(
919 contract_path,
920 contract.name.clone(),
921 version,
922 config,
923 settings.clone(),
924 )
925 } else {
926 if config.get_rpc_url().is_none() {
927 eyre::bail!("You have to provide a contract name or a valid RPC URL");
928 }
929 let provider = utils::get_provider(&config)?;
930 let code = provider.get_code_at(self.address).await?;
931
932 let output = ProjectCompiler::new().quiet(true).compile(&project)?;
933 let contracts = ContractsByArtifact::new(
934 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
935 );
936
937 let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
938 eyre::bail!(format!(
939 "Bytecode at {} does not match any local contracts",
940 self.address
941 ));
942 };
943
944 let settings = project
945 .settings_profiles()
946 .find_map(|(name, settings)| {
947 (name == artifact_id.profile.as_str()).then_some(settings)
948 })
949 .expect("must be present");
950
951 VerificationContext::new(
952 artifact_id.source.clone(),
953 artifact_id.name.split('.').next().unwrap().to_owned(),
954 artifact_id.version.clone(),
955 config,
956 settings.clone(),
957 )
958 }
959 }
960
961 pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
963 self.language.unwrap_or_else(|| {
964 match ctx.target_path.extension().and_then(|e| e.to_str()) {
965 Some("vy") => ContractLanguage::Vyper,
966 _ => ContractLanguage::Solidity,
967 }
968 })
969 }
970}
971
972#[derive(Clone, Debug, Parser)]
974pub struct VerifyCheckArgs {
975 pub id: String,
981
982 #[command(flatten)]
983 pub retry: RetryArgs,
984
985 #[command(flatten)]
986 pub etherscan: EtherscanOpts,
987
988 #[command(flatten)]
989 pub verifier: VerifierArgs,
990}
991
992impl_figment_convert_cast!(VerifyCheckArgs);
993
994impl VerifyCheckArgs {
995 pub async fn run(self) -> Result<()> {
997 sh_status!("Checking verification status on {}", self.etherscan.chain.unwrap_or_default())?;
998 self.verifier
999 .effective_type()
1000 .client(
1001 self.etherscan.key().as_deref(),
1002 self.etherscan.chain,
1003 self.verifier.verifier_url.is_some(),
1004 self.verifier.is_explicitly_set(),
1005 )?
1006 .check(self)
1007 .await
1008 }
1009}
1010
1011impl FigmentProvider for VerifyCheckArgs {
1012 fn metadata(&self) -> Metadata {
1013 Metadata::named("Verify Check Provider")
1014 }
1015
1016 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
1017 let mut dict = self.etherscan.dict();
1018 if let Some(api_key) = &self.etherscan.key {
1019 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
1020 }
1021
1022 Ok(Map::from([(Config::selected_profile(), dict)]))
1023 }
1024}
1025
1026pub fn sourcify_api_url(chain: Chain) -> Option<String> {
1031 if chain.is_custom_sourcify() {
1032 chain.etherscan_urls().map(|(api_url, _)| {
1033 let api_url = api_url.trim_end_matches('/');
1034 format!("{api_url}/")
1035 })
1036 } else {
1037 None
1038 }
1039}
1040
1041const fn is_dev_chain(chain: Chain) -> bool {
1043 use foundry_config::NamedChain;
1044 matches!(chain.named(), Some(NamedChain::Dev | NamedChain::AnvilHardhat | NamedChain::Cannon))
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050
1051 #[test]
1052 fn verifier_api_key_takes_precedence_over_etherscan_key() {
1053 let args = VerifierArgs { verifier_api_key: Some("explicit".into()), ..Default::default() };
1054 assert_eq!(args.resolve_api_key(Some("config")), Some("explicit"));
1055 assert_eq!(VerifierArgs::default().resolve_api_key(Some("config")), Some("config"));
1056 }
1057
1058 #[tokio::test]
1059 async fn external_context_rejects_control_characters_in_fqn() {
1060 let args =
1061 VerifyArgs::parse_from(["foundry-cli", "0xd8509bee9c9bf012282ad33aba0d87241baf5064"]);
1062 let context = ExternalVerificationContext {
1063 config: Config::default(),
1064 compiler_version: semver::Version::new(0, 8, 30),
1065 standard_json_input: std::sync::Arc::new(serde_json::json!({})),
1066 target: "A.sol:A\nforged".into(),
1067 };
1068 assert!(
1069 args.run_with_external_context(context)
1070 .await
1071 .unwrap_err()
1072 .to_string()
1073 .contains("printable")
1074 );
1075 }
1076
1077 #[test]
1078 fn external_run_context_clone_shares_standard_json() {
1079 let input = std::sync::Arc::new(serde_json::json!({"large": [1, 2, 3]}));
1080 let context = RunContext::External(Box::new(ExternalVerificationContext {
1081 config: Config::default(),
1082 compiler_version: semver::Version::new(0, 8, 30),
1083 standard_json_input: input.clone(),
1084 target: "A.sol:A".into(),
1085 }));
1086 let RunContext::External(context) = &context else { unreachable!() };
1087 let cloned = context.clone();
1088 assert!(std::sync::Arc::ptr_eq(&input, &cloned.standard_json_input));
1089 }
1090
1091 #[test]
1092 fn can_parse_verify_contract() {
1093 let args: VerifyArgs = VerifyArgs::parse_from([
1094 "foundry-cli",
1095 "0x0000000000000000000000000000000000000000",
1096 "src/Domains.sol:Domains",
1097 "--via-ir",
1098 "--license-type",
1099 "13",
1100 ]);
1101 assert!(args.via_ir);
1102 assert_eq!(args.license_type.as_deref(), Some("13"));
1103 }
1104
1105 #[test]
1106 fn can_parse_verify_contract_license_type_spdx() {
1107 let args: VerifyArgs = VerifyArgs::parse_from([
1108 "foundry-cli",
1109 "0x0000000000000000000000000000000000000000",
1110 "src/Domains.sol:Domains",
1111 "--license-type",
1112 "AGPL-3.0-or-later",
1113 ]);
1114 assert_eq!(args.license_type.as_deref(), Some("13"));
1115
1116 let args: VerifyArgs = VerifyArgs::parse_from([
1117 "foundry-cli",
1118 "0x0000000000000000000000000000000000000000",
1119 "src/Domains.sol:Domains",
1120 "--license-type",
1121 "MIT",
1122 ]);
1123 assert_eq!(args.license_type.as_deref(), Some("3"));
1124
1125 let args: VerifyArgs = VerifyArgs::parse_from([
1126 "foundry-cli",
1127 "0x0000000000000000000000000000000000000000",
1128 "src/Domains.sol:Domains",
1129 "--license-type",
1130 "apache 2.0",
1131 ]);
1132 assert_eq!(args.license_type.as_deref(), Some("12"));
1133 }
1134
1135 #[test]
1136 fn verify_contract_license_type_is_case_insensitive() {
1137 for variant in ["mit", "MIT", "Mit", "mIt"] {
1138 let args: VerifyArgs = VerifyArgs::parse_from([
1139 "foundry-cli",
1140 "0x0000000000000000000000000000000000000000",
1141 "src/Domains.sol:Domains",
1142 "--license-type",
1143 variant,
1144 ]);
1145 assert_eq!(args.license_type.as_deref(), Some("3"), "input: {variant}");
1146 }
1147 }
1148
1149 #[test]
1150 fn verify_contract_license_type_accepts_numeric_codes() {
1151 for (code, expected) in [("1", "1"), ("14", "14"), ("15", "15")] {
1152 let args: VerifyArgs = VerifyArgs::parse_from([
1153 "foundry-cli",
1154 "0x0000000000000000000000000000000000000000",
1155 "src/Domains.sol:Domains",
1156 "--license-type",
1157 code,
1158 ]);
1159 assert_eq!(args.license_type.as_deref(), Some(expected));
1160 }
1161 }
1162
1163 #[test]
1164 fn errors_on_invalid_verify_contract_license_type() {
1165 let err = VerifyArgs::try_parse_from([
1166 "foundry-cli",
1167 "0x0000000000000000000000000000000000000000",
1168 "src/Domains.sol:Domains",
1169 "--license-type",
1170 "Unknown-License",
1171 ])
1172 .unwrap_err();
1173 assert!(err.to_string().contains("unsupported Etherscan license type"));
1174 }
1175
1176 #[test]
1177 fn can_parse_new_compiler_flags() {
1178 let args: VerifyArgs = VerifyArgs::parse_from([
1179 "foundry-cli",
1180 "0x0000000000000000000000000000000000000000",
1181 "src/Domains.sol:Domains",
1182 "--no-auto-detect",
1183 "--use",
1184 "0.8.23",
1185 ]);
1186 assert!(args.no_auto_detect);
1187 assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
1188 }
1189
1190 #[test]
1191 fn classify_verifier_probe_accepts_not_verified_response() {
1192 let body =
1193 r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#;
1194 assert_eq!(
1195 classify_verifier_credential_response(StatusCode::OK, body),
1196 VerifierCredentialProbe::Accepted,
1197 );
1198 }
1199
1200 #[test]
1201 fn classify_verifier_probe_rejects_invalid_api_key() {
1202 let body = r#"{"status":"0","message":"NOTOK","result":"Invalid API Key"}"#;
1203 assert_eq!(
1204 classify_verifier_credential_response(StatusCode::OK, body),
1205 VerifierCredentialProbe::InvalidApiKey,
1206 );
1207 assert_eq!(
1208 classify_verifier_credential_response(StatusCode::UNAUTHORIZED, ""),
1209 VerifierCredentialProbe::InvalidApiKey,
1210 );
1211 }
1212
1213 #[test]
1214 fn classify_verifier_probe_treats_transient_errors_as_inconclusive() {
1215 let body = r#"{"status":"0","message":"NOTOK","result":"Max rate limit reached"}"#;
1216 assert_eq!(
1217 classify_verifier_credential_response(StatusCode::OK, body),
1218 VerifierCredentialProbe::Inconclusive,
1219 );
1220 assert_eq!(
1221 classify_verifier_credential_response(
1222 StatusCode::OK,
1223 "Checking if the site connection is secure",
1224 ),
1225 VerifierCredentialProbe::Inconclusive,
1226 );
1227 assert_eq!(
1228 classify_verifier_credential_response(
1229 StatusCode::FORBIDDEN,
1230 "Sorry, you have been blocked",
1231 ),
1232 VerifierCredentialProbe::Inconclusive,
1233 );
1234 assert_eq!(
1235 classify_verifier_credential_response(StatusCode::FORBIDDEN, ""),
1236 VerifierCredentialProbe::Inconclusive,
1237 );
1238 }
1239
1240 #[test]
1241 fn parse_http_verifier_url_rejects_unsupported_schemes() {
1242 assert!(parse_http_verifier_url("https://example.com/api", "verifier").is_ok());
1243 assert!(parse_http_verifier_url("http://example.com/api", "verifier").is_ok());
1244
1245 let err = parse_http_verifier_url("gopher://example.com/api", "verifier").unwrap_err();
1246 assert!(
1247 err.to_string().contains("URL scheme must be http or https"),
1248 "unexpected error: {err:?}"
1249 );
1250 }
1251
1252 #[test]
1253 fn resolve_explicit_sourcify_overrides_api_key() {
1254 let args = VerifierArgs {
1255 verifier: Some(VerificationProviderType::Sourcify),
1256 verifier_api_key: None,
1257 verifier_url: None,
1258 };
1259 assert_eq!(
1260 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1261 VerificationProviderType::Sourcify,
1262 );
1263 }
1264
1265 #[test]
1266 fn resolve_explicit_etherscan_is_etherscan() {
1267 let args = VerifierArgs {
1268 verifier: Some(VerificationProviderType::Etherscan),
1269 verifier_api_key: None,
1270 verifier_url: None,
1271 };
1272 assert_eq!(
1273 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1274 VerificationProviderType::Etherscan,
1275 );
1276 }
1277
1278 #[test]
1279 fn resolve_implicit_with_key_and_known_chain_uses_etherscan() {
1280 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1281 assert_eq!(
1282 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1283 VerificationProviderType::Etherscan,
1284 );
1285 }
1286
1287 #[test]
1288 fn resolve_implicit_with_key_and_unknown_chain_falls_back_to_sourcify() {
1289 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1290 assert_eq!(
1291 args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
1292 VerificationProviderType::Sourcify,
1293 );
1294 }
1295
1296 #[test]
1297 fn resolve_implicit_with_key_and_unknown_chain_but_url_uses_etherscan() {
1298 let args = VerifierArgs {
1299 verifier: None,
1300 verifier_api_key: None,
1301 verifier_url: Some("https://example.com/api".to_string()),
1302 };
1303 assert_eq!(
1304 args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
1305 VerificationProviderType::Etherscan,
1306 );
1307 }
1308
1309 #[test]
1310 fn resolve_implicit_no_key_falls_back_to_sourcify() {
1311 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1312 assert_eq!(args.resolve(None, Some(Chain::mainnet())), VerificationProviderType::Sourcify,);
1313 }
1314
1315 #[test]
1318 fn resolve_implicit_with_key_and_custom_sourcify_chain_falls_back_to_sourcify() {
1319 let tempo = Chain::from(4217u64); assert!(tempo.is_custom_sourcify(), "sanity: Tempo should be is_custom_sourcify");
1321 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1322 assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
1323 }
1324
1325 #[test]
1329 fn resolve_custom_sourcify_chain_with_url_and_key_stays_sourcify() {
1330 let tempo = Chain::from(4217u64);
1331 let args = VerifierArgs {
1332 verifier: None,
1333 verifier_api_key: None,
1334 verifier_url: Some("https://contracts.tempo.xyz/".to_string()),
1335 };
1336 assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
1337 }
1338
1339 #[test]
1340 fn collect_runs_adds_sourcify_provider() {
1341 let args: VerifyArgs = VerifyArgs::parse_from([
1342 "foundry-cli",
1343 "0x0000000000000000000000000000000000000000",
1344 "src/Counter.sol:Counter",
1345 "--etherscan-api-key",
1346 "k",
1347 ]);
1348 let resolved = args.verifier.resolve(Some("k"), Some(Chain::mainnet()));
1349 let runs = args.collect_runs(Chain::mainnet(), Some("k"), resolved, false).unwrap();
1350 assert_eq!(runs.len(), 2);
1351 assert_eq!(runs[0].label, VerificationProviderType::Etherscan);
1352 assert!(runs[0].required);
1353 assert_eq!(runs[1].label, VerificationProviderType::Sourcify);
1354 assert!(!runs[1].required);
1355 assert!(runs[1].args.verifier.verifier_api_key.is_none());
1356 }
1357
1358 #[test]
1359 fn collect_runs_skips_secondary_when_primary_is_sourcify() {
1360 let args: VerifyArgs = VerifyArgs::parse_from([
1361 "foundry-cli",
1362 "0x0000000000000000000000000000000000000000",
1363 "src/Counter.sol:Counter",
1364 "--verifier",
1365 "sourcify",
1366 ]);
1367 let resolved = args.verifier.resolve(None, Some(Chain::mainnet()));
1368 let runs = args.collect_runs(Chain::mainnet(), None, resolved, false).unwrap();
1369 assert_eq!(runs.len(), 1);
1370 assert_eq!(runs[0].label, VerificationProviderType::Sourcify);
1371 assert!(runs[0].required);
1372 }
1373
1374 #[test]
1375 fn collect_runs_skips_secondary_for_custom_verifier() {
1376 let args: VerifyArgs = VerifyArgs::parse_from([
1377 "foundry-cli",
1378 "0x0000000000000000000000000000000000000000",
1379 "src/Counter.sol:Counter",
1380 "--verifier",
1381 "custom",
1382 "--verifier-url",
1383 "https://internal.example.com/api",
1384 ]);
1385 let runs = args
1386 .collect_runs(Chain::mainnet(), None, VerificationProviderType::Custom, true)
1387 .unwrap();
1388 assert_eq!(runs.len(), 1);
1389 assert_eq!(runs[0].label, VerificationProviderType::Custom);
1390 }
1391
1392 #[test]
1393 fn collect_runs_skips_secondary_on_dev_chain() {
1394 let args: VerifyArgs = VerifyArgs::parse_from([
1395 "foundry-cli",
1396 "0x0000000000000000000000000000000000000000",
1397 "src/Counter.sol:Counter",
1398 "--etherscan-api-key",
1399 "k",
1400 ]);
1401 let anvil = Chain::from(31337u64);
1402 let resolved = args.verifier.resolve(Some("k"), Some(anvil));
1403 let runs = args.collect_runs(anvil, Some("k"), resolved, false).unwrap();
1404 assert_eq!(runs.len(), 1);
1405 assert!(runs[0].required);
1406 }
1407
1408 #[test]
1409 fn collect_runs_skips_secondary_with_user_verifier_url() {
1410 let args: VerifyArgs = VerifyArgs::parse_from([
1411 "foundry-cli",
1412 "0x0000000000000000000000000000000000000000",
1413 "src/Counter.sol:Counter",
1414 "--verifier",
1415 "blockscout",
1416 "--verifier-url",
1417 "https://internal-blockscout.example.com/api",
1418 ]);
1419 let resolved = args.verifier.resolve(None, Some(Chain::mainnet()));
1420 let runs = args.collect_runs(Chain::mainnet(), None, resolved, true).unwrap();
1421 assert_eq!(runs.len(), 1);
1422 assert_eq!(runs[0].label, VerificationProviderType::Blockscout);
1423 }
1424}