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, compilers::solc::Solc, info::ContractInfo};
23use foundry_config::{
24 Chain, Config, SolcReq,
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 config = self.load_config()?;
567 let context = self.resolve_context().await?;
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()?;
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 match solc {
829 SolcReq::Version(version) => version.to_owned(),
830 SolcReq::Local(solc) => Solc::new(solc)?.version,
831 }
832 } else if let Some(entry) =
833 cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
834 {
835 let unique_versions = entry
836 .artifacts
837 .get(&contract.name)
838 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
839 .unwrap_or_default();
840
841 if unique_versions.is_empty() {
842 eyre::bail!(
843 "No matching artifact found for {}. This could be due to:\n\
844 - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
845 contract.name
846 );
847 } else if unique_versions.len() > 1 {
848 warn!(
849 "Ambiguous compiler versions found in cache: {}",
850 unique_versions.iter().join(", ")
851 );
852 eyre::bail!(
853 "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
854 );
855 }
856
857 unique_versions.into_iter().next().unwrap().to_owned()
858 } else {
859 eyre::bail!(
860 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
861 );
862 };
863
864 let settings = if let Some(profile) = &self.compilation_profile {
865 if profile == "default" {
866 &project.settings
867 } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
868 settings
869 } else {
870 eyre::bail!("Unknown compilation profile: {}", profile);
871 }
872 } else if let Some((cache, entry)) = cache
873 .as_ref()
874 .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
875 {
876 let profiles = entry
877 .artifacts
878 .get(&contract.name)
879 .and_then(|artifacts| {
880 let mut cached_artifacts = artifacts.get(&version);
881 if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
889 version.build = BuildMetadata::EMPTY;
890 cached_artifacts = artifacts.get(&version);
891 }
892 cached_artifacts
893 })
894 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
895 .unwrap_or_default();
896
897 if profiles.is_empty() {
898 eyre::bail!(
899 "No matching artifact found for {} with compiler version {}. This could be due to:\n\
900 - Compiler version mismatch - the contract was compiled with a different Solidity version",
901 contract.name,
902 version
903 );
904 } else if profiles.len() > 1 {
905 eyre::bail!(
906 "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
907 profiles.iter().join(", ")
908 );
909 }
910
911 let profile = profiles.into_iter().next().unwrap().to_owned();
912 cache.profiles.get(&profile).expect("must be present")
913 } else if project.additional_settings.is_empty() {
914 &project.settings
915 } else {
916 eyre::bail!(
917 "If cache is disabled, compilation profile must be provided with `--compilation-profile` option or set in foundry.toml"
918 );
919 };
920
921 VerificationContext::new(
922 contract_path,
923 contract.name.clone(),
924 version,
925 config,
926 settings.clone(),
927 )
928 } else {
929 if config.get_rpc_url().is_none() {
930 eyre::bail!("You have to provide a contract name or a valid RPC URL");
931 }
932 let provider = utils::get_provider(&config)?;
933 let code = provider.get_code_at(self.address).await?;
934
935 let output = ProjectCompiler::new().quiet(true).compile(&project)?;
936 let contracts = ContractsByArtifact::new(
937 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
938 );
939
940 let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
941 eyre::bail!(format!(
942 "Bytecode at {} does not match any local contracts",
943 self.address
944 ));
945 };
946
947 let settings = project
948 .settings_profiles()
949 .find_map(|(name, settings)| {
950 (name == artifact_id.profile.as_str()).then_some(settings)
951 })
952 .expect("must be present");
953
954 VerificationContext::new(
955 artifact_id.source.clone(),
956 artifact_id.name.split('.').next().unwrap().to_owned(),
957 artifact_id.version.clone(),
958 config,
959 settings.clone(),
960 )
961 }
962 }
963
964 pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
966 self.language.unwrap_or_else(|| {
967 match ctx.target_path.extension().and_then(|e| e.to_str()) {
968 Some("vy") => ContractLanguage::Vyper,
969 _ => ContractLanguage::Solidity,
970 }
971 })
972 }
973}
974
975#[derive(Clone, Debug, Parser)]
977pub struct VerifyCheckArgs {
978 pub id: String,
984
985 #[command(flatten)]
986 pub retry: RetryArgs,
987
988 #[command(flatten)]
989 pub etherscan: EtherscanOpts,
990
991 #[command(flatten)]
992 pub verifier: VerifierArgs,
993}
994
995impl_figment_convert_cast!(VerifyCheckArgs);
996
997impl VerifyCheckArgs {
998 pub async fn run(self) -> Result<()> {
1000 sh_status!("Checking verification status on {}", self.etherscan.chain.unwrap_or_default())?;
1001 self.verifier
1002 .effective_type()
1003 .client(
1004 self.etherscan.key().as_deref(),
1005 self.etherscan.chain,
1006 self.verifier.verifier_url.is_some(),
1007 self.verifier.is_explicitly_set(),
1008 )?
1009 .check(self)
1010 .await
1011 }
1012}
1013
1014impl FigmentProvider for VerifyCheckArgs {
1015 fn metadata(&self) -> Metadata {
1016 Metadata::named("Verify Check Provider")
1017 }
1018
1019 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
1020 let mut dict = self.etherscan.dict();
1021 if let Some(api_key) = &self.etherscan.key {
1022 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
1023 }
1024
1025 Ok(Map::from([(Config::selected_profile(), dict)]))
1026 }
1027}
1028
1029pub fn sourcify_api_url(chain: Chain) -> Option<String> {
1034 if chain.is_custom_sourcify() {
1035 chain.etherscan_urls().map(|(api_url, _)| {
1036 let api_url = api_url.trim_end_matches('/');
1037 format!("{api_url}/")
1038 })
1039 } else {
1040 None
1041 }
1042}
1043
1044const fn is_dev_chain(chain: Chain) -> bool {
1046 use foundry_config::NamedChain;
1047 matches!(chain.named(), Some(NamedChain::Dev | NamedChain::AnvilHardhat | NamedChain::Cannon))
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::*;
1053
1054 #[test]
1055 fn verifier_api_key_takes_precedence_over_etherscan_key() {
1056 let args = VerifierArgs { verifier_api_key: Some("explicit".into()), ..Default::default() };
1057 assert_eq!(args.resolve_api_key(Some("config")), Some("explicit"));
1058 assert_eq!(VerifierArgs::default().resolve_api_key(Some("config")), Some("config"));
1059 }
1060
1061 #[tokio::test]
1062 async fn external_context_rejects_control_characters_in_fqn() {
1063 let args =
1064 VerifyArgs::parse_from(["foundry-cli", "0xd8509bee9c9bf012282ad33aba0d87241baf5064"]);
1065 let context = ExternalVerificationContext {
1066 config: Config::default(),
1067 compiler_version: semver::Version::new(0, 8, 30),
1068 standard_json_input: std::sync::Arc::new(serde_json::json!({})),
1069 target: "A.sol:A\nforged".into(),
1070 };
1071 assert!(
1072 args.run_with_external_context(context)
1073 .await
1074 .unwrap_err()
1075 .to_string()
1076 .contains("printable")
1077 );
1078 }
1079
1080 #[test]
1081 fn external_run_context_clone_shares_standard_json() {
1082 let input = std::sync::Arc::new(serde_json::json!({"large": [1, 2, 3]}));
1083 let context = RunContext::External(Box::new(ExternalVerificationContext {
1084 config: Config::default(),
1085 compiler_version: semver::Version::new(0, 8, 30),
1086 standard_json_input: input.clone(),
1087 target: "A.sol:A".into(),
1088 }));
1089 let RunContext::External(context) = &context else { unreachable!() };
1090 let cloned = context.clone();
1091 assert!(std::sync::Arc::ptr_eq(&input, &cloned.standard_json_input));
1092 }
1093
1094 #[test]
1095 fn can_parse_verify_contract() {
1096 let args: VerifyArgs = VerifyArgs::parse_from([
1097 "foundry-cli",
1098 "0x0000000000000000000000000000000000000000",
1099 "src/Domains.sol:Domains",
1100 "--via-ir",
1101 "--license-type",
1102 "13",
1103 ]);
1104 assert!(args.via_ir);
1105 assert_eq!(args.license_type.as_deref(), Some("13"));
1106 }
1107
1108 #[test]
1109 fn can_parse_verify_contract_license_type_spdx() {
1110 let args: VerifyArgs = VerifyArgs::parse_from([
1111 "foundry-cli",
1112 "0x0000000000000000000000000000000000000000",
1113 "src/Domains.sol:Domains",
1114 "--license-type",
1115 "AGPL-3.0-or-later",
1116 ]);
1117 assert_eq!(args.license_type.as_deref(), Some("13"));
1118
1119 let args: VerifyArgs = VerifyArgs::parse_from([
1120 "foundry-cli",
1121 "0x0000000000000000000000000000000000000000",
1122 "src/Domains.sol:Domains",
1123 "--license-type",
1124 "MIT",
1125 ]);
1126 assert_eq!(args.license_type.as_deref(), Some("3"));
1127
1128 let args: VerifyArgs = VerifyArgs::parse_from([
1129 "foundry-cli",
1130 "0x0000000000000000000000000000000000000000",
1131 "src/Domains.sol:Domains",
1132 "--license-type",
1133 "apache 2.0",
1134 ]);
1135 assert_eq!(args.license_type.as_deref(), Some("12"));
1136 }
1137
1138 #[test]
1139 fn verify_contract_license_type_is_case_insensitive() {
1140 for variant in ["mit", "MIT", "Mit", "mIt"] {
1141 let args: VerifyArgs = VerifyArgs::parse_from([
1142 "foundry-cli",
1143 "0x0000000000000000000000000000000000000000",
1144 "src/Domains.sol:Domains",
1145 "--license-type",
1146 variant,
1147 ]);
1148 assert_eq!(args.license_type.as_deref(), Some("3"), "input: {variant}");
1149 }
1150 }
1151
1152 #[test]
1153 fn verify_contract_license_type_accepts_numeric_codes() {
1154 for (code, expected) in [("1", "1"), ("14", "14"), ("15", "15")] {
1155 let args: VerifyArgs = VerifyArgs::parse_from([
1156 "foundry-cli",
1157 "0x0000000000000000000000000000000000000000",
1158 "src/Domains.sol:Domains",
1159 "--license-type",
1160 code,
1161 ]);
1162 assert_eq!(args.license_type.as_deref(), Some(expected));
1163 }
1164 }
1165
1166 #[test]
1167 fn errors_on_invalid_verify_contract_license_type() {
1168 let err = VerifyArgs::try_parse_from([
1169 "foundry-cli",
1170 "0x0000000000000000000000000000000000000000",
1171 "src/Domains.sol:Domains",
1172 "--license-type",
1173 "Unknown-License",
1174 ])
1175 .unwrap_err();
1176 assert!(err.to_string().contains("unsupported Etherscan license type"));
1177 }
1178
1179 #[test]
1180 fn can_parse_new_compiler_flags() {
1181 let args: VerifyArgs = VerifyArgs::parse_from([
1182 "foundry-cli",
1183 "0x0000000000000000000000000000000000000000",
1184 "src/Domains.sol:Domains",
1185 "--no-auto-detect",
1186 "--use",
1187 "0.8.23",
1188 ]);
1189 assert!(args.no_auto_detect);
1190 assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
1191 }
1192
1193 #[test]
1194 fn classify_verifier_probe_accepts_not_verified_response() {
1195 let body =
1196 r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#;
1197 assert_eq!(
1198 classify_verifier_credential_response(StatusCode::OK, body),
1199 VerifierCredentialProbe::Accepted,
1200 );
1201 }
1202
1203 #[test]
1204 fn classify_verifier_probe_rejects_invalid_api_key() {
1205 let body = r#"{"status":"0","message":"NOTOK","result":"Invalid API Key"}"#;
1206 assert_eq!(
1207 classify_verifier_credential_response(StatusCode::OK, body),
1208 VerifierCredentialProbe::InvalidApiKey,
1209 );
1210 assert_eq!(
1211 classify_verifier_credential_response(StatusCode::UNAUTHORIZED, ""),
1212 VerifierCredentialProbe::InvalidApiKey,
1213 );
1214 }
1215
1216 #[test]
1217 fn classify_verifier_probe_treats_transient_errors_as_inconclusive() {
1218 let body = r#"{"status":"0","message":"NOTOK","result":"Max rate limit reached"}"#;
1219 assert_eq!(
1220 classify_verifier_credential_response(StatusCode::OK, body),
1221 VerifierCredentialProbe::Inconclusive,
1222 );
1223 assert_eq!(
1224 classify_verifier_credential_response(
1225 StatusCode::OK,
1226 "Checking if the site connection is secure",
1227 ),
1228 VerifierCredentialProbe::Inconclusive,
1229 );
1230 assert_eq!(
1231 classify_verifier_credential_response(
1232 StatusCode::FORBIDDEN,
1233 "Sorry, you have been blocked",
1234 ),
1235 VerifierCredentialProbe::Inconclusive,
1236 );
1237 assert_eq!(
1238 classify_verifier_credential_response(StatusCode::FORBIDDEN, ""),
1239 VerifierCredentialProbe::Inconclusive,
1240 );
1241 }
1242
1243 #[test]
1244 fn parse_http_verifier_url_rejects_unsupported_schemes() {
1245 assert!(parse_http_verifier_url("https://example.com/api", "verifier").is_ok());
1246 assert!(parse_http_verifier_url("http://example.com/api", "verifier").is_ok());
1247
1248 let err = parse_http_verifier_url("gopher://example.com/api", "verifier").unwrap_err();
1249 assert!(
1250 err.to_string().contains("URL scheme must be http or https"),
1251 "unexpected error: {err:?}"
1252 );
1253 }
1254
1255 #[test]
1256 fn resolve_explicit_sourcify_overrides_api_key() {
1257 let args = VerifierArgs {
1258 verifier: Some(VerificationProviderType::Sourcify),
1259 verifier_api_key: None,
1260 verifier_url: None,
1261 };
1262 assert_eq!(
1263 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1264 VerificationProviderType::Sourcify,
1265 );
1266 }
1267
1268 #[test]
1269 fn resolve_explicit_etherscan_is_etherscan() {
1270 let args = VerifierArgs {
1271 verifier: Some(VerificationProviderType::Etherscan),
1272 verifier_api_key: None,
1273 verifier_url: None,
1274 };
1275 assert_eq!(
1276 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1277 VerificationProviderType::Etherscan,
1278 );
1279 }
1280
1281 #[test]
1282 fn resolve_implicit_with_key_and_known_chain_uses_etherscan() {
1283 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1284 assert_eq!(
1285 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1286 VerificationProviderType::Etherscan,
1287 );
1288 }
1289
1290 #[test]
1291 fn resolve_implicit_with_key_and_unknown_chain_falls_back_to_sourcify() {
1292 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1293 assert_eq!(
1294 args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
1295 VerificationProviderType::Sourcify,
1296 );
1297 }
1298
1299 #[test]
1300 fn resolve_implicit_with_key_and_unknown_chain_but_url_uses_etherscan() {
1301 let args = VerifierArgs {
1302 verifier: None,
1303 verifier_api_key: None,
1304 verifier_url: Some("https://example.com/api".to_string()),
1305 };
1306 assert_eq!(
1307 args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
1308 VerificationProviderType::Etherscan,
1309 );
1310 }
1311
1312 #[test]
1313 fn resolve_implicit_no_key_falls_back_to_sourcify() {
1314 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1315 assert_eq!(args.resolve(None, Some(Chain::mainnet())), VerificationProviderType::Sourcify,);
1316 }
1317
1318 #[test]
1321 fn resolve_implicit_with_key_and_custom_sourcify_chain_falls_back_to_sourcify() {
1322 let tempo = Chain::from(4217u64); assert!(tempo.is_custom_sourcify(), "sanity: Tempo should be is_custom_sourcify");
1324 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1325 assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
1326 }
1327
1328 #[test]
1332 fn resolve_custom_sourcify_chain_with_url_and_key_stays_sourcify() {
1333 let tempo = Chain::from(4217u64);
1334 let args = VerifierArgs {
1335 verifier: None,
1336 verifier_api_key: None,
1337 verifier_url: Some("https://contracts.tempo.xyz/".to_string()),
1338 };
1339 assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
1340 }
1341
1342 #[test]
1343 fn collect_runs_adds_sourcify_provider() {
1344 let args: VerifyArgs = VerifyArgs::parse_from([
1345 "foundry-cli",
1346 "0x0000000000000000000000000000000000000000",
1347 "src/Counter.sol:Counter",
1348 "--etherscan-api-key",
1349 "k",
1350 ]);
1351 let resolved = args.verifier.resolve(Some("k"), Some(Chain::mainnet()));
1352 let runs = args.collect_runs(Chain::mainnet(), Some("k"), resolved, false).unwrap();
1353 assert_eq!(runs.len(), 2);
1354 assert_eq!(runs[0].label, VerificationProviderType::Etherscan);
1355 assert!(runs[0].required);
1356 assert_eq!(runs[1].label, VerificationProviderType::Sourcify);
1357 assert!(!runs[1].required);
1358 assert!(runs[1].args.verifier.verifier_api_key.is_none());
1359 }
1360
1361 #[test]
1362 fn collect_runs_skips_secondary_when_primary_is_sourcify() {
1363 let args: VerifyArgs = VerifyArgs::parse_from([
1364 "foundry-cli",
1365 "0x0000000000000000000000000000000000000000",
1366 "src/Counter.sol:Counter",
1367 "--verifier",
1368 "sourcify",
1369 ]);
1370 let resolved = args.verifier.resolve(None, Some(Chain::mainnet()));
1371 let runs = args.collect_runs(Chain::mainnet(), None, resolved, false).unwrap();
1372 assert_eq!(runs.len(), 1);
1373 assert_eq!(runs[0].label, VerificationProviderType::Sourcify);
1374 assert!(runs[0].required);
1375 }
1376
1377 #[test]
1378 fn collect_runs_skips_secondary_for_custom_verifier() {
1379 let args: VerifyArgs = VerifyArgs::parse_from([
1380 "foundry-cli",
1381 "0x0000000000000000000000000000000000000000",
1382 "src/Counter.sol:Counter",
1383 "--verifier",
1384 "custom",
1385 "--verifier-url",
1386 "https://internal.example.com/api",
1387 ]);
1388 let runs = args
1389 .collect_runs(Chain::mainnet(), None, VerificationProviderType::Custom, true)
1390 .unwrap();
1391 assert_eq!(runs.len(), 1);
1392 assert_eq!(runs[0].label, VerificationProviderType::Custom);
1393 }
1394
1395 #[test]
1396 fn collect_runs_skips_secondary_on_dev_chain() {
1397 let args: VerifyArgs = VerifyArgs::parse_from([
1398 "foundry-cli",
1399 "0x0000000000000000000000000000000000000000",
1400 "src/Counter.sol:Counter",
1401 "--etherscan-api-key",
1402 "k",
1403 ]);
1404 let anvil = Chain::from(31337u64);
1405 let resolved = args.verifier.resolve(Some("k"), Some(anvil));
1406 let runs = args.collect_runs(anvil, Some("k"), resolved, false).unwrap();
1407 assert_eq!(runs.len(), 1);
1408 assert!(runs[0].required);
1409 }
1410
1411 #[test]
1412 fn collect_runs_skips_secondary_with_user_verifier_url() {
1413 let args: VerifyArgs = VerifyArgs::parse_from([
1414 "foundry-cli",
1415 "0x0000000000000000000000000000000000000000",
1416 "src/Counter.sol:Counter",
1417 "--verifier",
1418 "blockscout",
1419 "--verifier-url",
1420 "https://internal-blockscout.example.com/api",
1421 ]);
1422 let resolved = args.verifier.resolve(None, Some(Chain::mainnet()));
1423 let runs = args.collect_runs(Chain::mainnet(), None, resolved, true).unwrap();
1424 assert_eq!(runs.len(), 1);
1425 assert_eq!(runs[0].label, VerificationProviderType::Blockscout);
1426 }
1427}