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