Skip to main content

forge_verify/
verify.rs

1//! The `forge verify-bytecode` command.
2
3use crate::{
4    RetryArgs,
5    etherscan::EtherscanVerificationProvider,
6    provider::{VerificationContext, VerificationProvider, VerificationProviderType},
7    utils::wrap_verifier_url_error,
8};
9use alloy_primitives::{Address, TxHash, map::HashSet};
10use alloy_provider::Provider;
11use clap::{Parser, ValueEnum, ValueHint};
12use eyre::{Context, Result};
13use foundry_cli::{
14    opts::{EtherscanOpts, RpcOpts},
15    utils::{self, LoadConfig},
16};
17use foundry_common::{ContractsByArtifact, compile::ProjectCompiler};
18use foundry_compilers::{artifacts::EvmVersion, compilers::solc::Solc, info::ContractInfo};
19use foundry_config::{
20    Chain, Config, SolcReq,
21    figment::{
22        Error, Metadata, Profile, Provider as FigmentProvider,
23        value::{Dict, Map, Value},
24    },
25    impl_figment_convert, impl_figment_convert_cast,
26};
27use itertools::Itertools;
28use reqwest::{Client, StatusCode, Url};
29use semver::BuildMetadata;
30use serde::Deserialize;
31use std::{path::PathBuf, time::Duration};
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34enum VerifierCredentialProbe {
35    Accepted,
36    InvalidApiKey,
37    Inconclusive,
38}
39
40#[derive(Debug, Deserialize)]
41struct EtherscanProbeResponse {
42    status: String,
43    result: Option<serde_json::Value>,
44}
45
46fn verifier_credential_probe_query(api_key: Option<&str>) -> Vec<(&'static str, String)> {
47    let mut query = vec![
48        ("module", "contract".to_string()),
49        ("action", "getabi".to_string()),
50        ("address", Address::ZERO.to_string()),
51    ];
52    if let Some(api_key) = api_key {
53        query.push(("apikey", api_key.to_string()));
54    }
55    query
56}
57
58fn classify_verifier_credential_response(
59    status: StatusCode,
60    body: &str,
61) -> VerifierCredentialProbe {
62    let lower = body.to_lowercase();
63    if lower.contains("invalid api key") || lower.contains("invalid_api_key") {
64        return VerifierCredentialProbe::InvalidApiKey;
65    }
66
67    if lower.contains("contract source code not verified")
68        || lower.contains("contract not found")
69        || lower.contains("contract was not found")
70    {
71        return VerifierCredentialProbe::Accepted;
72    }
73
74    if status == StatusCode::UNAUTHORIZED {
75        return VerifierCredentialProbe::InvalidApiKey;
76    }
77
78    if !status.is_success()
79        || lower.contains("max rate limit reached")
80        || lower.contains("sorry, you have been blocked")
81        || lower.contains("checking if the site connection is secure")
82    {
83        return VerifierCredentialProbe::Inconclusive;
84    }
85
86    match serde_json::from_str::<EtherscanProbeResponse>(body) {
87        Ok(resp) if resp.status == "1" => VerifierCredentialProbe::Accepted,
88        Ok(resp) => resp
89            .result
90            .and_then(|result| result.as_str().map(str::to_lowercase))
91            .map(|result| {
92                if result.contains("invalid api key") || result.contains("invalid_api_key") {
93                    VerifierCredentialProbe::InvalidApiKey
94                } else if result.contains("max rate limit reached") {
95                    VerifierCredentialProbe::Inconclusive
96                } else if result.contains("contract source code not verified")
97                    || result.contains("contract not found")
98                    || result.contains("contract was not found")
99                {
100                    VerifierCredentialProbe::Accepted
101                } else {
102                    VerifierCredentialProbe::Inconclusive
103                }
104            })
105            .unwrap_or(VerifierCredentialProbe::Inconclusive),
106        Err(_) => VerifierCredentialProbe::Inconclusive,
107    }
108}
109
110fn parse_http_verifier_url(url: &str, label: &str) -> Result<Url> {
111    let url = Url::parse(url).wrap_err_with(|| format!("invalid {label} URL `{url}`"))?;
112    if !matches!(url.scheme(), "http" | "https") {
113        eyre::bail!("invalid {label} URL `{url}`: URL scheme must be http or https");
114    }
115    Ok(url)
116}
117
118async fn probe_verifier_credentials(
119    url: Url,
120    api_key: Option<&str>,
121) -> Result<VerifierCredentialProbe, reqwest::Error> {
122    let resp = Client::new()
123        .get(url)
124        .query(&verifier_credential_probe_query(api_key))
125        .timeout(Duration::from_secs(10))
126        .send()
127        .await?;
128    let status = resp.status();
129    let body = resp.text().await?;
130    Ok(classify_verifier_credential_response(status, &body))
131}
132
133/// The programming language used for smart contract development.
134///
135/// This enum represents the supported contract languages for verification.
136#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
137pub enum ContractLanguage {
138    /// Solidity programming language
139    Solidity,
140    /// Vyper programming language
141    Vyper,
142}
143
144/// Verification provider arguments
145#[derive(Clone, Debug, Default, Parser)]
146pub struct VerifierArgs {
147    /// The contract verification provider to use.
148    #[arg(long, help_heading = "Verifier options", value_enum)]
149    pub verifier: Option<VerificationProviderType>,
150
151    /// The verifier API KEY, if using a custom provider.
152    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_KEY")]
153    pub verifier_api_key: Option<String>,
154
155    /// The verifier URL, if using a custom provider.
156    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")]
157    pub verifier_url: Option<String>,
158}
159
160impl VerifierArgs {
161    /// Returns the effective verifier type, defaulting to Sourcify if not explicitly set.
162    ///
163    /// Note: this is the *defaulted CLI value*, not the actually-selected provider after
164    /// considering `ETHERSCAN_API_KEY` / chain support. Use [`Self::resolve`] for that.
165    pub fn effective_type(&self) -> VerificationProviderType {
166        self.verifier.unwrap_or_default()
167    }
168
169    /// Returns true if `--verifier` was explicitly provided by the user.
170    pub const fn is_explicitly_set(&self) -> bool {
171        self.verifier.is_some()
172    }
173
174    /// Resolves the API key with consistent precedence: explicit `--verifier-api-key` first,
175    /// then the etherscan config key.
176    pub fn resolve_api_key<'a>(&'a self, etherscan_key: Option<&'a str>) -> Option<&'a str> {
177        self.verifier_api_key.as_deref().or(etherscan_key)
178    }
179
180    /// Makes a lightweight network call to validate that credentials are accepted by the verifier.
181    ///
182    /// `api_key` must be the already-merged API key (CLI `--verifier-api-key` takes
183    /// precedence over config), as returned by [`Self::resolve_api_key`].
184    pub async fn check_credentials(
185        &self,
186        api_key: Option<&str>,
187        chain: Chain,
188        config: &Config,
189    ) -> eyre::Result<()> {
190        let resolved = self.resolve(api_key, Some(chain));
191        match resolved {
192            VerificationProviderType::Etherscan
193            | VerificationProviderType::Blockscout
194            | VerificationProviderType::Oklink => {
195                let etherscan_opts =
196                    EtherscanOpts { key: api_key.map(str::to_owned), chain: Some(chain) };
197                let client = EtherscanVerificationProvider::default().client(
198                    &etherscan_opts,
199                    self,
200                    config,
201                )?;
202                match tokio::time::timeout(
203                    Duration::from_secs(10),
204                    probe_verifier_credentials(
205                        client.etherscan_api_url().clone(),
206                        client.api_key(),
207                    ),
208                )
209                .await
210                {
211                    Err(_) => {
212                        sh_warn!("verifier credential check timed out, proceeding anyway")?;
213                    }
214                    Ok(Ok(VerifierCredentialProbe::Accepted)) => {}
215                    Ok(Ok(VerifierCredentialProbe::InvalidApiKey)) => {
216                        eyre::bail!("verifier credential check failed: invalid API key");
217                    }
218                    Ok(Ok(VerifierCredentialProbe::Inconclusive) | Err(_)) => {
219                        sh_warn!("verifier credential check inconclusive, proceeding anyway")?;
220                    }
221                }
222            }
223            VerificationProviderType::Custom => {
224                // Custom verifiers may return Etherscan-shaped responses (HTTP 200 with a JSON
225                // body) or standard HTTP auth errors (401/403). Check both.
226                if let Some(url) = &self.verifier_url {
227                    let url = parse_http_verifier_url(url, "verifier")?;
228                    match probe_verifier_credentials(url, api_key).await {
229                        Err(_) => {
230                            sh_warn!("verifier credential check failed, proceeding anyway")?;
231                        }
232                        Ok(
233                            VerifierCredentialProbe::Accepted
234                            | VerifierCredentialProbe::Inconclusive,
235                        ) => {}
236                        Ok(VerifierCredentialProbe::InvalidApiKey) => {
237                            eyre::bail!("verifier credential check failed: invalid API key")
238                        }
239                    }
240                }
241            }
242            VerificationProviderType::Sourcify => {
243                // Only probe custom URLs; the default public endpoint is assumed reachable.
244                if let Some(url) = &self.verifier_url {
245                    let url = parse_http_verifier_url(url, "Sourcify")?;
246                    match Client::new()
247                        .get(url.clone())
248                        .timeout(Duration::from_secs(10))
249                        .send()
250                        .await
251                    {
252                        Err(_) => {
253                            sh_warn!(
254                                "Sourcify URL `{url}` could not be reached, proceeding anyway"
255                            )?;
256                        }
257                        Ok(resp) => {
258                            let status = resp.status();
259                            if !status.is_success() && status != StatusCode::NOT_FOUND {
260                                sh_warn!(
261                                    "Sourcify URL `{url}` returned HTTP {status}, proceeding anyway"
262                                )?;
263                            }
264                        }
265                    }
266                }
267            }
268        }
269        Ok(())
270    }
271
272    /// Resolves the actual verification provider that will be used at runtime, taking into
273    /// account the explicit `--verifier`, the presence of `ETHERSCAN_API_KEY`, and whether the
274    /// target chain has a known Etherscan API URL.
275    ///
276    /// Resolution rules (mirrors [`VerificationProviderType::client`]):
277    /// 1. If `--verifier` was explicitly set, that wins.
278    /// 2. Otherwise, if an Etherscan API key is set AND the chain is supported (or a custom
279    ///    `--verifier-url` is provided), use Etherscan.
280    /// 3. Otherwise, fall back to Sourcify.
281    pub fn resolve(
282        &self,
283        etherscan_key: Option<&str>,
284        chain: Option<Chain>,
285    ) -> VerificationProviderType {
286        if let Some(v) = self.verifier {
287            return v;
288        }
289        let has_key = etherscan_key.is_some_and(|k| !k.is_empty());
290        // Custom-Sourcify chains (e.g. Tempo) register Sourcify-compatible URLs under
291        // etherscan_urls() but are NOT real Etherscan chains. Skip the implicit-Etherscan path
292        // entirely for them; the caller must use `--verifier etherscan` explicitly to override.
293        if has_key && !chain.is_some_and(|c| c.is_custom_sourcify()) {
294            let chain_has_etherscan_url = chain.is_none_or(|c| c.etherscan_urls().is_some());
295            if chain_has_etherscan_url || self.verifier_url.is_some() {
296                return VerificationProviderType::Etherscan;
297            }
298        }
299        VerificationProviderType::Sourcify
300    }
301}
302
303/// CLI arguments for `forge verify-contract`.
304#[derive(Clone, Debug, Parser)]
305pub struct VerifyArgs {
306    /// The address of the contract to verify.
307    pub address: Address,
308
309    /// The contract identifier in the form `<path>:<contractname>`.
310    pub contract: Option<ContractInfo>,
311
312    /// The ABI-encoded constructor arguments. Only for Etherscan.
313    #[arg(
314        long,
315        conflicts_with = "constructor_args_path",
316        value_name = "ARGS",
317        visible_alias = "encoded-constructor-args"
318    )]
319    pub constructor_args: Option<String>,
320
321    /// The path to a file containing the constructor arguments.
322    #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
323    pub constructor_args_path: Option<PathBuf>,
324
325    /// Try to extract constructor arguments from on-chain creation code.
326    #[arg(long)]
327    pub guess_constructor_args: bool,
328
329    /// The hash of the transaction which created the contract. Optional for Sourcify.
330    #[arg(long)]
331    pub creation_transaction_hash: Option<TxHash>,
332
333    /// The `solc` version to use to build the smart contract.
334    #[arg(long, value_name = "VERSION")]
335    pub compiler_version: Option<String>,
336
337    /// The compilation profile to use to build the smart contract.
338    #[arg(long, value_name = "PROFILE_NAME")]
339    pub compilation_profile: Option<String>,
340
341    /// The number of optimization runs used to build the smart contract.
342    #[arg(long, visible_alias = "optimizer-runs", value_name = "NUM")]
343    pub num_of_optimizations: Option<usize>,
344
345    /// Flatten the source code before verifying.
346    #[arg(long)]
347    pub flatten: bool,
348
349    /// Do not compile the flattened smart contract before verifying (if --flatten is passed).
350    #[arg(short, long)]
351    pub force: bool,
352
353    /// Do not check if the contract is already verified before verifying.
354    #[arg(long)]
355    pub skip_is_verified_check: bool,
356
357    /// Wait for verification result after submission.
358    #[arg(long)]
359    pub watch: bool,
360
361    /// Set pre-linked libraries.
362    #[arg(long, help_heading = "Linker options", env = "DAPP_LIBRARIES")]
363    pub libraries: Vec<String>,
364
365    /// The project's root path.
366    ///
367    /// By default root of the Git repository, if in one,
368    /// or the current working directory.
369    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
370    pub root: Option<PathBuf>,
371
372    /// Prints the standard json compiler input.
373    ///
374    /// The standard json compiler input can be used to manually submit contract verification in
375    /// the browser.
376    #[arg(long, conflicts_with = "flatten")]
377    pub show_standard_json_input: bool,
378
379    /// Use the Yul intermediate representation compilation pipeline.
380    #[arg(long)]
381    pub via_ir: bool,
382
383    /// The Etherscan license type code to include with the verification request.
384    ///
385    /// See Etherscan's supported `licenseType` values. This is only used for Etherscan-style
386    /// verifiers.
387    #[arg(long, value_name = "CODE", help_heading = "Verifier options")]
388    pub license_type: Option<String>,
389
390    /// The EVM version to use.
391    ///
392    /// Overrides the version specified in the config.
393    #[arg(long)]
394    pub evm_version: Option<EvmVersion>,
395
396    /// Do not auto-detect the `solc` version.
397    #[arg(long, help_heading = "Compiler options")]
398    pub no_auto_detect: bool,
399
400    /// Specify the solc version, or a path to a local solc, to build with.
401    ///
402    /// Valid values are in the format `x.y.z`, `solc:x.y.z` or `path/to/solc`.
403    #[arg(long = "use", help_heading = "Compiler options", value_name = "SOLC_VERSION")]
404    pub use_solc: Option<String>,
405
406    #[command(flatten)]
407    pub etherscan: EtherscanOpts,
408
409    #[command(flatten)]
410    pub rpc: RpcOpts,
411
412    #[command(flatten)]
413    pub retry: RetryArgs,
414
415    #[command(flatten)]
416    pub verifier: VerifierArgs,
417
418    /// The contract language (`solidity` or `vyper`).
419    ///
420    /// Defaults to `solidity` if none provided.
421    #[arg(long, value_enum)]
422    pub language: Option<ContractLanguage>,
423}
424
425impl_figment_convert!(VerifyArgs);
426
427impl FigmentProvider for VerifyArgs {
428    fn metadata(&self) -> Metadata {
429        Metadata::named("Verify Provider")
430    }
431
432    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
433        let mut dict = self.etherscan.dict();
434        dict.extend(self.rpc.dict());
435
436        if let Some(root) = self.root.as_ref() {
437            dict.insert("root".to_string(), Value::serialize(root)?);
438        }
439        if let Some(optimizer_runs) = self.num_of_optimizations {
440            dict.insert("optimizer".to_string(), Value::serialize(true)?);
441            dict.insert("optimizer_runs".to_string(), Value::serialize(optimizer_runs)?);
442        }
443        if let Some(evm_version) = self.evm_version {
444            dict.insert("evm_version".to_string(), Value::serialize(evm_version)?);
445        }
446        if self.via_ir {
447            dict.insert("via_ir".to_string(), Value::serialize(self.via_ir)?);
448        }
449
450        if self.no_auto_detect {
451            dict.insert("auto_detect_solc".to_string(), Value::serialize(false)?);
452        }
453
454        if let Some(ref solc) = self.use_solc {
455            let solc = solc.trim_start_matches("solc:");
456            dict.insert("solc".to_string(), Value::serialize(solc)?);
457        }
458
459        if let Some(api_key) = &self.verifier.verifier_api_key {
460            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
461        }
462
463        Ok(Map::from([(Config::selected_profile(), dict)]))
464    }
465}
466
467impl VerifyArgs {
468    /// Run the verify command to submit the contract's source code for verification on etherscan
469    pub async fn run(mut self) -> Result<()> {
470        let config = self.load_config()?;
471
472        if self.guess_constructor_args && config.get_rpc_url().is_none() {
473            eyre::bail!(
474                "You have to provide a valid RPC URL to use --guess-constructor-args feature"
475            )
476        }
477
478        // If chain is not set, we try to get it from the RPC.
479        // If RPC is not set, the default chain is used.
480        let chain = match config.get_rpc_url() {
481            Some(_) => {
482                let provider = utils::get_provider(&config)?;
483                utils::get_chain(config.chain, provider).await?
484            }
485            None => config.chain.unwrap_or_default(),
486        };
487
488        let context = self.resolve_context().await?;
489
490        // Set Etherscan options.
491        self.etherscan.chain = Some(chain);
492        // `get_etherscan_config_with_chain` returns None for chains with no known Etherscan API
493        // URL (even when a key was explicitly passed), because `ResolvedEtherscanConfig::create`
494        // requires `chain.etherscan_urls()`. Fall back to the raw `etherscan_api_key` from config
495        // so that the key survives for warning/fallback logic in `client()`.
496        self.etherscan.key = config
497            .get_etherscan_config_with_chain(Some(chain))?
498            .map(|c| c.key)
499            .or_else(|| config.etherscan_api_key.clone());
500
501        // Capture whether the user explicitly provided a verifier URL *before* any auto-injection.
502        // This is passed to `client()` so that an auto-injected Sourcify URL does not look like a
503        // user-supplied Etherscan-compatible URL and cause the wrong provider to be selected.
504        let had_user_verifier_url = self.verifier.verifier_url.is_some();
505
506        // Resolve provider BEFORE URL injection so that the auto-injected Sourcify URL cannot
507        // influence routing. For custom-Sourcify chains (e.g. Tempo), etherscan_urls() returns
508        // Some but is_custom_sourcify() excludes them from the Etherscan path in resolve().
509        let etherscan_key = self.etherscan.key();
510        let resolved = self.verifier.resolve(etherscan_key.as_deref(), self.etherscan.chain);
511
512        // For chains with Sourcify-compatible APIs, inject their URL only when we've resolved to
513        // Sourcify and the user did not already supply a --verifier-url.
514        if resolved.is_sourcify()
515            && !had_user_verifier_url
516            && let Some(url) = sourcify_api_url(chain)
517        {
518            self.verifier.verifier_url = Some(url);
519        }
520
521        if self.show_standard_json_input {
522            let args = EtherscanVerificationProvider::default()
523                .create_verify_request(&self, &context)
524                .await?;
525            sh_println!("{}", args.source)?;
526            return Ok(());
527        }
528
529        let verifier_url = self.verifier.verifier_url.clone();
530        sh_status!("Start verifying contract `{}` deployed on {chain}", self.address)?;
531        if let Some(version) = &self.evm_version {
532            sh_status!("EVM version: {version}")?;
533        }
534        if let Some(version) = &self.compiler_version {
535            sh_status!("Compiler version: {version}")?;
536        }
537        if let Some(optimizations) = &self.num_of_optimizations {
538            sh_status!("Optimizations:    {optimizations}")?
539        }
540        if let Some(args) = &self.constructor_args
541            && !args.is_empty()
542        {
543            sh_status!("Constructor args: {args}")?
544        }
545        let using_etherscan = resolved.is_etherscan();
546        resolved
547            .client(
548                etherscan_key.as_deref(),
549                self.etherscan.chain,
550                had_user_verifier_url,
551                self.verifier.is_explicitly_set(),
552            )?
553            .verify(self, context)
554            .await
555            .map_err(|err| wrap_verifier_url_error(err, verifier_url.as_deref(), using_etherscan))
556    }
557
558    /// Returns the configured verification provider
559    pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
560        self.verifier.effective_type().client(
561            self.etherscan.key().as_deref(),
562            self.etherscan.chain,
563            self.verifier.verifier_url.is_some(),
564            self.verifier.is_explicitly_set(),
565        )
566    }
567
568    /// Resolves [VerificationContext] object either from entered contract name or by trying to
569    /// match bytecode located at given address.
570    pub async fn resolve_context(&self) -> Result<VerificationContext> {
571        let mut config = self.load_config()?;
572        config.libraries.extend(self.libraries.clone());
573
574        let project = config.project()?;
575
576        if let Some(ref contract) = self.contract {
577            let contract_path = if let Some(ref path) = contract.path {
578                project.root().join(PathBuf::from(path))
579            } else {
580                project.find_contract_path(&contract.name)?
581            };
582
583            let cache = project.read_cache_file().ok();
584
585            let mut version = if let Some(ref version) = self.compiler_version {
586                version.trim_start_matches('v').parse()?
587            } else if let Some(ref solc) = config.solc {
588                match solc {
589                    SolcReq::Version(version) => version.to_owned(),
590                    SolcReq::Local(solc) => Solc::new(solc)?.version,
591                }
592            } else if let Some(entry) =
593                cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
594            {
595                let unique_versions = entry
596                    .artifacts
597                    .get(&contract.name)
598                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
599                    .unwrap_or_default();
600
601                if unique_versions.is_empty() {
602                    eyre::bail!(
603                        "No matching artifact found for {}. This could be due to:\n\
604                        - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
605                        contract.name
606                    );
607                } else if unique_versions.len() > 1 {
608                    warn!(
609                        "Ambiguous compiler versions found in cache: {}",
610                        unique_versions.iter().join(", ")
611                    );
612                    eyre::bail!(
613                        "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
614                    )
615                }
616
617                unique_versions.into_iter().next().unwrap().to_owned()
618            } else {
619                eyre::bail!(
620                    "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
621                )
622            };
623
624            let settings = if let Some(profile) = &self.compilation_profile {
625                if profile == "default" {
626                    &project.settings
627                } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
628                    settings
629                } else {
630                    eyre::bail!("Unknown compilation profile: {}", profile)
631                }
632            } else if let Some((cache, entry)) = cache
633                .as_ref()
634                .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
635            {
636                let profiles = entry
637                    .artifacts
638                    .get(&contract.name)
639                    .and_then(|artifacts| {
640                        let mut cached_artifacts = artifacts.get(&version);
641                        // If we try to verify with specific build version and no cached artifacts
642                        // found, then check if we have artifacts cached for same version but
643                        // without any build metadata.
644                        // This could happen when artifacts are built / cached
645                        // with a version like `0.8.20` but verify is using a compiler-version arg
646                        // as `0.8.20+commit.a1b79de6`.
647                        // See <https://github.com/foundry-rs/foundry/issues/9510>.
648                        if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
649                            version.build = BuildMetadata::EMPTY;
650                            cached_artifacts = artifacts.get(&version);
651                        }
652                        cached_artifacts
653                    })
654                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
655                    .unwrap_or_default();
656
657                if profiles.is_empty() {
658                    eyre::bail!(
659                        "No matching artifact found for {} with compiler version {}. This could be due to:\n\
660                        - Compiler version mismatch - the contract was compiled with a different Solidity version",
661                        contract.name,
662                        version
663                    );
664                } else if profiles.len() > 1 {
665                    eyre::bail!(
666                        "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
667                        profiles.iter().join(", ")
668                    )
669                }
670
671                let profile = profiles.into_iter().next().unwrap().to_owned();
672                cache.profiles.get(&profile).expect("must be present")
673            } else if project.additional_settings.is_empty() {
674                &project.settings
675            } else {
676                eyre::bail!(
677                    "If cache is disabled, compilation profile must be provided with `--compilation-profile` option or set in foundry.toml"
678                )
679            };
680
681            VerificationContext::new(
682                contract_path,
683                contract.name.clone(),
684                version,
685                config,
686                settings.clone(),
687            )
688        } else {
689            if config.get_rpc_url().is_none() {
690                eyre::bail!("You have to provide a contract name or a valid RPC URL")
691            }
692            let provider = utils::get_provider(&config)?;
693            let code = provider.get_code_at(self.address).await?;
694
695            let output = ProjectCompiler::new().quiet(true).compile(&project)?;
696            let contracts = ContractsByArtifact::new(
697                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
698            );
699
700            let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
701                eyre::bail!(format!(
702                    "Bytecode at {} does not match any local contracts",
703                    self.address
704                ))
705            };
706
707            let settings = project
708                .settings_profiles()
709                .find_map(|(name, settings)| {
710                    (name == artifact_id.profile.as_str()).then_some(settings)
711                })
712                .expect("must be present");
713
714            VerificationContext::new(
715                artifact_id.source.clone(),
716                artifact_id.name.split('.').next().unwrap().to_owned(),
717                artifact_id.version.clone(),
718                config,
719                settings.clone(),
720            )
721        }
722    }
723
724    /// Detects the language for verification from source file extension, if none provided.
725    pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
726        self.language.unwrap_or_else(|| {
727            match ctx.target_path.extension().and_then(|e| e.to_str()) {
728                Some("vy") => ContractLanguage::Vyper,
729                _ => ContractLanguage::Solidity,
730            }
731        })
732    }
733}
734
735/// Check verification status arguments
736#[derive(Clone, Debug, Parser)]
737pub struct VerifyCheckArgs {
738    /// The verification ID.
739    ///
740    /// For Etherscan - Submission GUID.
741    ///
742    /// For Sourcify - Verification Job ID.
743    pub id: String,
744
745    #[command(flatten)]
746    pub retry: RetryArgs,
747
748    #[command(flatten)]
749    pub etherscan: EtherscanOpts,
750
751    #[command(flatten)]
752    pub verifier: VerifierArgs,
753}
754
755impl_figment_convert_cast!(VerifyCheckArgs);
756
757impl VerifyCheckArgs {
758    /// Run the verify command to submit the contract's source code for verification on etherscan
759    pub async fn run(self) -> Result<()> {
760        sh_status!("Checking verification status on {}", self.etherscan.chain.unwrap_or_default())?;
761        self.verifier
762            .effective_type()
763            .client(
764                self.etherscan.key().as_deref(),
765                self.etherscan.chain,
766                self.verifier.verifier_url.is_some(),
767                self.verifier.is_explicitly_set(),
768            )?
769            .check(self)
770            .await
771    }
772}
773
774impl FigmentProvider for VerifyCheckArgs {
775    fn metadata(&self) -> Metadata {
776        Metadata::named("Verify Check Provider")
777    }
778
779    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
780        let mut dict = self.etherscan.dict();
781        if let Some(api_key) = &self.etherscan.key {
782            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
783        }
784
785        Ok(Map::from([(Config::selected_profile(), dict)]))
786    }
787}
788
789/// Returns the Sourcify-compatible API URL for chains that have one registered in `etherscan_urls`.
790///
791/// Some chains register their Sourcify-compatible verification API under `etherscan_urls` in
792/// alloy-chains. This function returns the properly formatted URL for such chains.
793fn sourcify_api_url(chain: Chain) -> Option<String> {
794    if chain.is_custom_sourcify() {
795        chain.etherscan_urls().map(|(api_url, _)| {
796            let api_url = api_url.trim_end_matches('/');
797            format!("{api_url}/")
798        })
799    } else {
800        None
801    }
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807
808    #[test]
809    fn can_parse_verify_contract() {
810        let args: VerifyArgs = VerifyArgs::parse_from([
811            "foundry-cli",
812            "0x0000000000000000000000000000000000000000",
813            "src/Domains.sol:Domains",
814            "--via-ir",
815            "--license-type",
816            "13",
817        ]);
818        assert!(args.via_ir);
819        assert_eq!(args.license_type.as_deref(), Some("13"));
820    }
821
822    #[test]
823    fn can_parse_new_compiler_flags() {
824        let args: VerifyArgs = VerifyArgs::parse_from([
825            "foundry-cli",
826            "0x0000000000000000000000000000000000000000",
827            "src/Domains.sol:Domains",
828            "--no-auto-detect",
829            "--use",
830            "0.8.23",
831        ]);
832        assert!(args.no_auto_detect);
833        assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
834    }
835
836    #[test]
837    fn classify_verifier_probe_accepts_not_verified_response() {
838        let body =
839            r#"{"status":"0","message":"NOTOK","result":"Contract source code not verified"}"#;
840        assert_eq!(
841            classify_verifier_credential_response(StatusCode::OK, body),
842            VerifierCredentialProbe::Accepted,
843        );
844    }
845
846    #[test]
847    fn classify_verifier_probe_rejects_invalid_api_key() {
848        let body = r#"{"status":"0","message":"NOTOK","result":"Invalid API Key"}"#;
849        assert_eq!(
850            classify_verifier_credential_response(StatusCode::OK, body),
851            VerifierCredentialProbe::InvalidApiKey,
852        );
853        assert_eq!(
854            classify_verifier_credential_response(StatusCode::UNAUTHORIZED, ""),
855            VerifierCredentialProbe::InvalidApiKey,
856        );
857    }
858
859    #[test]
860    fn classify_verifier_probe_treats_transient_errors_as_inconclusive() {
861        let body = r#"{"status":"0","message":"NOTOK","result":"Max rate limit reached"}"#;
862        assert_eq!(
863            classify_verifier_credential_response(StatusCode::OK, body),
864            VerifierCredentialProbe::Inconclusive,
865        );
866        assert_eq!(
867            classify_verifier_credential_response(
868                StatusCode::OK,
869                "Checking if the site connection is secure",
870            ),
871            VerifierCredentialProbe::Inconclusive,
872        );
873        assert_eq!(
874            classify_verifier_credential_response(
875                StatusCode::FORBIDDEN,
876                "Sorry, you have been blocked",
877            ),
878            VerifierCredentialProbe::Inconclusive,
879        );
880        assert_eq!(
881            classify_verifier_credential_response(StatusCode::FORBIDDEN, ""),
882            VerifierCredentialProbe::Inconclusive,
883        );
884    }
885
886    #[test]
887    fn parse_http_verifier_url_rejects_unsupported_schemes() {
888        assert!(parse_http_verifier_url("https://example.com/api", "verifier").is_ok());
889        assert!(parse_http_verifier_url("http://example.com/api", "verifier").is_ok());
890
891        let err = parse_http_verifier_url("gopher://example.com/api", "verifier").unwrap_err();
892        assert!(
893            err.to_string().contains("URL scheme must be http or https"),
894            "unexpected error: {err:?}"
895        );
896    }
897
898    #[test]
899    fn resolve_explicit_sourcify_overrides_api_key() {
900        let args = VerifierArgs {
901            verifier: Some(VerificationProviderType::Sourcify),
902            verifier_api_key: None,
903            verifier_url: None,
904        };
905        assert_eq!(
906            args.resolve(Some("mykey"), Some(Chain::mainnet())),
907            VerificationProviderType::Sourcify,
908        );
909    }
910
911    #[test]
912    fn resolve_explicit_etherscan_is_etherscan() {
913        let args = VerifierArgs {
914            verifier: Some(VerificationProviderType::Etherscan),
915            verifier_api_key: None,
916            verifier_url: None,
917        };
918        assert_eq!(
919            args.resolve(Some("mykey"), Some(Chain::mainnet())),
920            VerificationProviderType::Etherscan,
921        );
922    }
923
924    #[test]
925    fn resolve_implicit_with_key_and_known_chain_uses_etherscan() {
926        let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
927        assert_eq!(
928            args.resolve(Some("mykey"), Some(Chain::mainnet())),
929            VerificationProviderType::Etherscan,
930        );
931    }
932
933    #[test]
934    fn resolve_implicit_with_key_and_unknown_chain_falls_back_to_sourcify() {
935        let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
936        assert_eq!(
937            args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
938            VerificationProviderType::Sourcify,
939        );
940    }
941
942    #[test]
943    fn resolve_implicit_with_key_and_unknown_chain_but_url_uses_etherscan() {
944        let args = VerifierArgs {
945            verifier: None,
946            verifier_api_key: None,
947            verifier_url: Some("https://example.com/api".to_string()),
948        };
949        assert_eq!(
950            args.resolve(Some("mykey"), Some(Chain::from(3658348u64))),
951            VerificationProviderType::Etherscan,
952        );
953    }
954
955    #[test]
956    fn resolve_implicit_no_key_falls_back_to_sourcify() {
957        let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
958        assert_eq!(args.resolve(None, Some(Chain::mainnet())), VerificationProviderType::Sourcify,);
959    }
960
961    // Regression: custom-Sourcify chains (e.g. Tempo) register Sourcify-compatible URLs under
962    // etherscan_urls(). An implicit ETHERSCAN_API_KEY must NOT route them to Etherscan.
963    #[test]
964    fn resolve_implicit_with_key_and_custom_sourcify_chain_falls_back_to_sourcify() {
965        let tempo = Chain::from(4217u64); // NamedChain::Tempo
966        assert!(tempo.is_custom_sourcify(), "sanity: Tempo should be is_custom_sourcify");
967        let args = VerifierArgs { verifier: None, verifier_api_key: None, verifier_url: None };
968        assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
969    }
970
971    // Ensure the is_custom_sourcify() guard holds even when a URL is present (e.g. the URL was
972    // auto-injected by run()). A user-supplied --verifier-url on a custom-Sourcify chain with a
973    // key should still resolve to Sourcify, not Etherscan.
974    #[test]
975    fn resolve_custom_sourcify_chain_with_url_and_key_stays_sourcify() {
976        let tempo = Chain::from(4217u64);
977        let args = VerifierArgs {
978            verifier: None,
979            verifier_api_key: None,
980            verifier_url: Some("https://contracts.tempo.xyz/".to_string()),
981        };
982        assert_eq!(args.resolve(Some("mykey"), Some(tempo)), VerificationProviderType::Sourcify,);
983    }
984}