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, _)) =
938 contracts.find_by_deployed_code_exact_unambiguous(&code)?
939 else {
940 eyre::bail!(format!(
941 "Bytecode at {} does not match any local contracts",
942 self.address
943 ));
944 };
945
946 let settings = project
947 .settings_profiles()
948 .find_map(|(name, settings)| {
949 (name == artifact_id.profile.as_str()).then_some(settings)
950 })
951 .expect("must be present");
952
953 VerificationContext::new(
954 artifact_id.source.clone(),
955 artifact_id.name.split('.').next().unwrap().to_owned(),
956 artifact_id.version.clone(),
957 config,
958 settings.clone(),
959 )
960 }
961 }
962
963 pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
965 self.language.unwrap_or_else(|| {
966 match ctx.target_path.extension().and_then(|e| e.to_str()) {
967 Some("vy") => ContractLanguage::Vyper,
968 _ => ContractLanguage::Solidity,
969 }
970 })
971 }
972}
973
974#[derive(Clone, Debug, Parser)]
976pub struct VerifyCheckArgs {
977 pub id: String,
983
984 #[command(flatten)]
985 pub retry: RetryArgs,
986
987 #[command(flatten)]
988 pub etherscan: EtherscanOpts,
989
990 #[command(flatten)]
991 pub verifier: VerifierArgs,
992}
993
994impl_figment_convert_cast!(VerifyCheckArgs);
995
996impl VerifyCheckArgs {
997 pub async fn run(mut self) -> Result<()> {
999 self.etherscan.key =
1000 self.verifier.resolve_api_key(self.etherscan.key.as_deref()).map(str::to_owned);
1001 sh_status!("Checking verification status on {}", self.etherscan.chain.unwrap_or_default())?;
1002 self.verifier
1003 .effective_type()
1004 .client(
1005 self.etherscan.key().as_deref(),
1006 self.etherscan.chain,
1007 self.verifier.verifier_url.is_some(),
1008 self.verifier.is_explicitly_set(),
1009 )?
1010 .check(self)
1011 .await
1012 }
1013}
1014
1015impl FigmentProvider for VerifyCheckArgs {
1016 fn metadata(&self) -> Metadata {
1017 Metadata::named("Verify Check Provider")
1018 }
1019
1020 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
1021 let mut dict = self.etherscan.dict();
1022 if let Some(api_key) = &self.etherscan.key {
1023 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
1024 }
1025
1026 Ok(Map::from([(Config::selected_profile(), dict)]))
1027 }
1028}
1029
1030pub fn sourcify_api_url(chain: Chain) -> Option<String> {
1035 if chain.is_custom_sourcify() {
1036 chain.etherscan_urls().map(|(api_url, _)| {
1037 let api_url = api_url.trim_end_matches('/');
1038 format!("{api_url}/")
1039 })
1040 } else {
1041 None
1042 }
1043}
1044
1045const fn is_dev_chain(chain: Chain) -> bool {
1047 use foundry_config::NamedChain;
1048 matches!(chain.named(), Some(NamedChain::Dev | NamedChain::AnvilHardhat | NamedChain::Cannon))
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::*;
1054
1055 #[test]
1056 fn verifier_api_key_takes_precedence_over_etherscan_key() {
1057 let args = VerifierArgs { verifier_api_key: Some("explicit".into()), ..Default::default() };
1058 assert_eq!(args.resolve_api_key(Some("config")), Some("explicit"));
1059 assert_eq!(VerifierArgs::default().resolve_api_key(Some("config")), Some("config"));
1060 }
1061
1062 #[tokio::test]
1063 async fn external_context_rejects_control_characters_in_fqn() {
1064 let args =
1065 VerifyArgs::parse_from(["foundry-cli", "0xd8509bee9c9bf012282ad33aba0d87241baf5064"]);
1066 let context = ExternalVerificationContext {
1067 config: Config::default(),
1068 compiler_version: semver::Version::new(0, 8, 30),
1069 standard_json_input: std::sync::Arc::new(serde_json::json!({})),
1070 target: "A.sol:A\nforged".into(),
1071 };
1072 assert!(
1073 args.run_with_external_context(context)
1074 .await
1075 .unwrap_err()
1076 .to_string()
1077 .contains("printable")
1078 );
1079 }
1080
1081 #[test]
1082 fn external_run_context_clone_shares_standard_json() {
1083 let input = std::sync::Arc::new(serde_json::json!({"large": [1, 2, 3]}));
1084 let context = RunContext::External(Box::new(ExternalVerificationContext {
1085 config: Config::default(),
1086 compiler_version: semver::Version::new(0, 8, 30),
1087 standard_json_input: input.clone(),
1088 target: "A.sol:A".into(),
1089 }));
1090 let RunContext::External(context) = &context else { unreachable!() };
1091 let cloned = context.clone();
1092 assert!(std::sync::Arc::ptr_eq(&input, &cloned.standard_json_input));
1093 }
1094
1095 #[test]
1096 fn can_parse_verify_contract() {
1097 let args: VerifyArgs = VerifyArgs::parse_from([
1098 "foundry-cli",
1099 "0x0000000000000000000000000000000000000000",
1100 "src/Domains.sol:Domains",
1101 "--via-ir",
1102 "--license-type",
1103 "13",
1104 ]);
1105 assert!(args.via_ir);
1106 assert_eq!(args.license_type.as_deref(), Some("13"));
1107 }
1108
1109 #[test]
1110 fn can_parse_verify_contract_license_type_spdx() {
1111 let args: VerifyArgs = VerifyArgs::parse_from([
1112 "foundry-cli",
1113 "0x0000000000000000000000000000000000000000",
1114 "src/Domains.sol:Domains",
1115 "--license-type",
1116 "AGPL-3.0-or-later",
1117 ]);
1118 assert_eq!(args.license_type.as_deref(), Some("13"));
1119
1120 let args: VerifyArgs = VerifyArgs::parse_from([
1121 "foundry-cli",
1122 "0x0000000000000000000000000000000000000000",
1123 "src/Domains.sol:Domains",
1124 "--license-type",
1125 "MIT",
1126 ]);
1127 assert_eq!(args.license_type.as_deref(), Some("3"));
1128
1129 let args: VerifyArgs = VerifyArgs::parse_from([
1130 "foundry-cli",
1131 "0x0000000000000000000000000000000000000000",
1132 "src/Domains.sol:Domains",
1133 "--license-type",
1134 "apache 2.0",
1135 ]);
1136 assert_eq!(args.license_type.as_deref(), Some("12"));
1137 }
1138
1139 #[test]
1140 fn verify_contract_license_type_is_case_insensitive() {
1141 for variant in ["mit", "MIT", "Mit", "mIt"] {
1142 let args: VerifyArgs = VerifyArgs::parse_from([
1143 "foundry-cli",
1144 "0x0000000000000000000000000000000000000000",
1145 "src/Domains.sol:Domains",
1146 "--license-type",
1147 variant,
1148 ]);
1149 assert_eq!(args.license_type.as_deref(), Some("3"), "input: {variant}");
1150 }
1151 }
1152
1153 #[test]
1154 fn verify_contract_license_type_accepts_numeric_codes() {
1155 for (code, expected) in [("1", "1"), ("14", "14"), ("15", "15")] {
1156 let args: VerifyArgs = VerifyArgs::parse_from([
1157 "foundry-cli",
1158 "0x0000000000000000000000000000000000000000",
1159 "src/Domains.sol:Domains",
1160 "--license-type",
1161 code,
1162 ]);
1163 assert_eq!(args.license_type.as_deref(), Some(expected));
1164 }
1165 }
1166
1167 #[test]
1168 fn errors_on_invalid_verify_contract_license_type() {
1169 let err = VerifyArgs::try_parse_from([
1170 "foundry-cli",
1171 "0x0000000000000000000000000000000000000000",
1172 "src/Domains.sol:Domains",
1173 "--license-type",
1174 "Unknown-License",
1175 ])
1176 .unwrap_err();
1177 assert!(err.to_string().contains("unsupported Etherscan license type"));
1178 }
1179
1180 #[test]
1181 fn can_parse_new_compiler_flags() {
1182 let args: VerifyArgs = VerifyArgs::parse_from([
1183 "foundry-cli",
1184 "0x0000000000000000000000000000000000000000",
1185 "src/Domains.sol:Domains",
1186 "--no-auto-detect",
1187 "--use",
1188 "0.8.23",
1189 ]);
1190 assert!(args.no_auto_detect);
1191 assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
1192 }
1193
1194 #[test]
1195 fn classify_verifier_probe_accepts_not_verified_response() {
1196 let body =
1197 r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#;
1198 assert_eq!(
1199 classify_verifier_credential_response(StatusCode::OK, body),
1200 VerifierCredentialProbe::Accepted,
1201 );
1202 }
1203
1204 #[test]
1205 fn classify_verifier_probe_rejects_invalid_api_key() {
1206 let body = r#"{"status":"0","message":"NOTOK","result":"Invalid API Key"}"#;
1207 assert_eq!(
1208 classify_verifier_credential_response(StatusCode::OK, body),
1209 VerifierCredentialProbe::InvalidApiKey,
1210 );
1211 assert_eq!(
1212 classify_verifier_credential_response(StatusCode::UNAUTHORIZED, ""),
1213 VerifierCredentialProbe::InvalidApiKey,
1214 );
1215 }
1216
1217 #[test]
1218 fn classify_verifier_probe_treats_transient_errors_as_inconclusive() {
1219 let body = r#"{"status":"0","message":"NOTOK","result":"Max rate limit reached"}"#;
1220 assert_eq!(
1221 classify_verifier_credential_response(StatusCode::OK, body),
1222 VerifierCredentialProbe::Inconclusive,
1223 );
1224 assert_eq!(
1225 classify_verifier_credential_response(
1226 StatusCode::OK,
1227 "Checking if the site connection is secure",
1228 ),
1229 VerifierCredentialProbe::Inconclusive,
1230 );
1231 assert_eq!(
1232 classify_verifier_credential_response(
1233 StatusCode::FORBIDDEN,
1234 "Sorry, you have been blocked",
1235 ),
1236 VerifierCredentialProbe::Inconclusive,
1237 );
1238 assert_eq!(
1239 classify_verifier_credential_response(StatusCode::FORBIDDEN, ""),
1240 VerifierCredentialProbe::Inconclusive,
1241 );
1242 }
1243
1244 #[test]
1245 fn parse_http_verifier_url_rejects_unsupported_schemes() {
1246 assert!(parse_http_verifier_url("https://example.com/api", "verifier").is_ok());
1247 assert!(parse_http_verifier_url("http://example.com/api", "verifier").is_ok());
1248
1249 let err = parse_http_verifier_url("gopher://example.com/api", "verifier").unwrap_err();
1250 assert!(
1251 err.to_string().contains("URL scheme must be http or https"),
1252 "unexpected error: {err:?}"
1253 );
1254 }
1255
1256 #[test]
1257 fn resolve_explicit_sourcify_overrides_api_key() {
1258 let args = VerifierArgs {
1259 verifier: Some(VerificationProviderType::Sourcify),
1260 verifier_api_key: None,
1261 verifier_url: None,
1262 };
1263 assert_eq!(
1264 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1265 VerificationProviderType::Sourcify,
1266 );
1267 }
1268
1269 #[test]
1270 fn resolve_explicit_etherscan_is_etherscan() {
1271 let args = VerifierArgs {
1272 verifier: Some(VerificationProviderType::Etherscan),
1273 verifier_api_key: None,
1274 verifier_url: None,
1275 };
1276 assert_eq!(
1277 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1278 VerificationProviderType::Etherscan,
1279 );
1280 }
1281
1282 #[test]
1283 fn resolve_implicit_with_key_and_known_chain_uses_etherscan() {
1284 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1285 assert_eq!(
1286 args.resolve(Some("mykey"), Some(Chain::mainnet())),
1287 VerificationProviderType::Etherscan,
1288 );
1289 }
1290
1291 #[test]
1292 fn resolve_implicit_with_key_and_unknown_chain_falls_back_to_sourcify() {
1293 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1294 assert_eq!(
1295 args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
1296 VerificationProviderType::Sourcify,
1297 );
1298 }
1299
1300 #[test]
1301 fn resolve_implicit_with_key_and_unknown_chain_but_url_uses_etherscan() {
1302 let args = VerifierArgs {
1303 verifier: None,
1304 verifier_api_key: None,
1305 verifier_url: Some("https://example.com/api".to_string()),
1306 };
1307 assert_eq!(
1308 args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
1309 VerificationProviderType::Etherscan,
1310 );
1311 }
1312
1313 #[test]
1314 fn resolve_implicit_no_key_falls_back_to_sourcify() {
1315 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1316 assert_eq!(args.resolve(None, Some(Chain::mainnet())), VerificationProviderType::Sourcify,);
1317 }
1318
1319 #[test]
1322 fn resolve_implicit_with_key_and_custom_sourcify_chain_falls_back_to_sourcify() {
1323 let tempo = Chain::from(4217u64); assert!(tempo.is_custom_sourcify(), "sanity: Tempo should be is_custom_sourcify");
1325 let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
1326 assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
1327 }
1328
1329 #[test]
1333 fn resolve_custom_sourcify_chain_with_url_and_key_stays_sourcify() {
1334 let tempo = Chain::from(4217u64);
1335 let args = VerifierArgs {
1336 verifier: None,
1337 verifier_api_key: None,
1338 verifier_url: Some("https://contracts.tempo.xyz/".to_string()),
1339 };
1340 assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
1341 }
1342
1343 #[test]
1344 fn collect_runs_adds_sourcify_provider() {
1345 let args: VerifyArgs = VerifyArgs::parse_from([
1346 "foundry-cli",
1347 "0x0000000000000000000000000000000000000000",
1348 "src/Counter.sol:Counter",
1349 "--etherscan-api-key",
1350 "k",
1351 ]);
1352 let resolved = args.verifier.resolve(Some("k"), Some(Chain::mainnet()));
1353 let runs = args.collect_runs(Chain::mainnet(), Some("k"), resolved, false).unwrap();
1354 assert_eq!(runs.len(), 2);
1355 assert_eq!(runs[0].label, VerificationProviderType::Etherscan);
1356 assert!(runs[0].required);
1357 assert_eq!(runs[1].label, VerificationProviderType::Sourcify);
1358 assert!(!runs[1].required);
1359 assert!(runs[1].args.verifier.verifier_api_key.is_none());
1360 }
1361
1362 #[test]
1363 fn collect_runs_skips_secondary_when_primary_is_sourcify() {
1364 let args: VerifyArgs = VerifyArgs::parse_from([
1365 "foundry-cli",
1366 "0x0000000000000000000000000000000000000000",
1367 "src/Counter.sol:Counter",
1368 "--verifier",
1369 "sourcify",
1370 ]);
1371 let resolved = args.verifier.resolve(None, Some(Chain::mainnet()));
1372 let runs = args.collect_runs(Chain::mainnet(), None, resolved, false).unwrap();
1373 assert_eq!(runs.len(), 1);
1374 assert_eq!(runs[0].label, VerificationProviderType::Sourcify);
1375 assert!(runs[0].required);
1376 }
1377
1378 #[test]
1379 fn collect_runs_skips_secondary_for_custom_verifier() {
1380 let args: VerifyArgs = VerifyArgs::parse_from([
1381 "foundry-cli",
1382 "0x0000000000000000000000000000000000000000",
1383 "src/Counter.sol:Counter",
1384 "--verifier",
1385 "custom",
1386 "--verifier-url",
1387 "https://internal.example.com/api",
1388 ]);
1389 let runs = args
1390 .collect_runs(Chain::mainnet(), None, VerificationProviderType::Custom, true)
1391 .unwrap();
1392 assert_eq!(runs.len(), 1);
1393 assert_eq!(runs[0].label, VerificationProviderType::Custom);
1394 }
1395
1396 #[test]
1397 fn collect_runs_skips_secondary_on_dev_chain() {
1398 let args: VerifyArgs = VerifyArgs::parse_from([
1399 "foundry-cli",
1400 "0x0000000000000000000000000000000000000000",
1401 "src/Counter.sol:Counter",
1402 "--etherscan-api-key",
1403 "k",
1404 ]);
1405 let anvil = Chain::from(31337u64);
1406 let resolved = args.verifier.resolve(Some("k"), Some(anvil));
1407 let runs = args.collect_runs(anvil, Some("k"), resolved, false).unwrap();
1408 assert_eq!(runs.len(), 1);
1409 assert!(runs[0].required);
1410 }
1411
1412 #[test]
1413 fn collect_runs_skips_secondary_with_user_verifier_url() {
1414 let args: VerifyArgs = VerifyArgs::parse_from([
1415 "foundry-cli",
1416 "0x0000000000000000000000000000000000000000",
1417 "src/Counter.sol:Counter",
1418 "--verifier",
1419 "blockscout",
1420 "--verifier-url",
1421 "https://internal-blockscout.example.com/api",
1422 ]);
1423 let resolved = args.verifier.resolve(None, Some(Chain::mainnet()));
1424 let runs = args.collect_runs(Chain::mainnet(), None, resolved, true).unwrap();
1425 assert_eq!(runs.len(), 1);
1426 assert_eq!(runs[0].label, VerificationProviderType::Blockscout);
1427 }
1428}