Skip to main content

forge_verify/
provider.rs

1use crate::{
2    etherscan::EtherscanVerificationProvider,
3    sourcify::SourcifyVerificationProvider,
4    verify::{VerifyArgs, VerifyCheckArgs},
5};
6use alloy_json_abi::JsonAbi;
7use async_trait::async_trait;
8use eyre::{Context, Result};
9use foundry_common::compile::compile_target_abi;
10use foundry_compilers::{
11    Project,
12    artifacts::{Source, StandardJsonCompilerInput, vyper::VyperInput},
13    compilers::solc::SolcCompiler,
14    multi::MultiCompilerSettings,
15    solc::{Solc, SolcLanguage},
16};
17use foundry_config::{Chain, Config, EtherscanConfigError};
18use semver::Version;
19use std::{
20    fmt,
21    future::Future as StdFuture,
22    path::{Path, PathBuf},
23    pin::Pin,
24    str::FromStr,
25    sync::Arc,
26};
27
28/// Container with data required for contract verification.
29#[derive(Debug, Clone)]
30pub struct VerificationContext {
31    pub config: Config,
32    pub project: Project,
33    pub target_path: PathBuf,
34    pub target_name: String,
35    pub compiler_version: Version,
36    pub compiler_settings: MultiCompilerSettings,
37}
38
39/// Caller-provided source material used to verify a contract.
40#[derive(Debug, Clone)]
41pub struct ExternalVerificationContext {
42    pub config: Config,
43    pub compiler_version: Version,
44    pub standard_json_input: Arc<serde_json::Value>,
45    pub target: String,
46}
47
48impl VerificationContext {
49    pub fn new(
50        target_path: PathBuf,
51        target_name: String,
52        compiler_version: Version,
53        config: Config,
54        compiler_settings: MultiCompilerSettings,
55    ) -> Result<Self> {
56        let mut project = config.project()?;
57        project.no_artifacts = true;
58
59        let solc = Solc::find_or_install(&compiler_version)?;
60        project.compiler.solc = Some(SolcCompiler::Specific(solc));
61
62        Ok(Self { config, project, target_name, target_path, compiler_version, compiler_settings })
63    }
64
65    pub fn get_solc_standard_json_input(&self) -> Result<StandardJsonCompilerInput> {
66        let mut input: StandardJsonCompilerInput = self
67            .project
68            .standard_json_input(&self.target_path)
69            .wrap_err("Failed to get standard json input")?
70            .normalize_evm_version(&self.compiler_version);
71
72        let mut settings = self.compiler_settings.solc.settings.clone();
73        settings.libraries.libs = input
74            .settings
75            .libraries
76            .libs
77            .into_iter()
78            .map(|(f, libs)| {
79                (f.strip_prefix(self.project.root()).unwrap_or(&f).to_path_buf(), libs)
80            })
81            .collect();
82
83        settings.remappings = input.settings.remappings;
84        settings.sanitize(&self.compiler_version, SolcLanguage::Solidity);
85        input.settings = settings;
86
87        Ok(input)
88    }
89
90    /// Creates Vyper standard JSON input for verification.
91    pub fn get_vyper_standard_json_input(&self) -> Result<VyperInput> {
92        let path = Path::new(&self.target_path);
93        let sources = Source::read_all_from(path, &["vy", "vyi"])?;
94        Ok(VyperInput::new(sources, self.compiler_settings.vyper.clone(), &self.compiler_version))
95    }
96
97    /// Compiles target contract requesting only ABI and returns it.
98    pub fn get_target_abi(&self) -> Result<JsonAbi> {
99        let mut project = self.project.clone();
100        compile_target_abi(&mut project, &self.target_path, &self.target_name)
101    }
102}
103
104/// An abstraction for various verification providers such as etherscan, sourcify, blockscout
105#[async_trait]
106pub trait VerificationProvider {
107    /// Returns the provider type, used to assert the selected provider in tests.
108    fn provider_type(&self) -> VerificationProviderType;
109
110    /// This should ensure the verify request can be prepared successfully.
111    ///
112    /// Caution: Implementers must ensure that this _never_ sends the actual verify request
113    /// `[VerificationProvider::verify]`, instead this is supposed to evaluate whether the given
114    /// [`VerifyArgs`] are valid to begin with. This should prevent situations where there's a
115    /// contract deployment that's executed before the verify request and the subsequent verify task
116    /// fails due to misconfiguration.
117    async fn preflight_verify_check(
118        &mut self,
119        args: VerifyArgs,
120        context: VerificationContext,
121    ) -> Result<()>;
122
123    /// Submits the verification request for the targeted contract.
124    ///
125    /// Returns `Some(check_args)` if a follow-up status check is possible (the request was
126    /// accepted by the provider), or `None` if the submission was a no-op (e.g. the contract
127    /// was already verified).
128    async fn submit(
129        &mut self,
130        args: VerifyArgs,
131        context: VerificationContext,
132    ) -> Result<Option<VerifyCheckArgs>>;
133
134    /// Submits caller-provided Standard JSON input.
135    fn submit_external(
136        &mut self,
137        _args: VerifyArgs,
138        _context: ExternalVerificationContext,
139    ) -> Pin<Box<dyn StdFuture<Output = Result<Option<VerifyCheckArgs>>> + '_>> {
140        Box::pin(async {
141            Err(eyre::eyre!("external verification is unsupported by this provider"))
142        })
143    }
144
145    /// Convenience wrapper: [`Self::submit`]s and, if `args.watch` is set, polls
146    /// [`Self::check`] until completion.
147    async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
148        let watch = args.watch;
149        let check_args = self.submit(args, context).await?;
150        if watch && let Some(check_args) = check_args {
151            return self.check(check_args).await;
152        }
153        Ok(())
154    }
155
156    /// Checks whether the contract is verified.
157    async fn check(&self, args: VerifyCheckArgs) -> Result<()>;
158}
159
160impl FromStr for VerificationProviderType {
161    type Err = String;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        match s {
165            "e" | "etherscan" => Ok(Self::Etherscan),
166            "s" | "sourcify" => Ok(Self::Sourcify),
167            "b" | "blockscout" => Ok(Self::Blockscout),
168            "o" | "oklink" => Ok(Self::Oklink),
169            "c" | "custom" => Ok(Self::Custom),
170            _ => Err(format!("Unknown provider: {s}")),
171        }
172    }
173}
174
175impl fmt::Display for VerificationProviderType {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Self::Etherscan => {
179                write!(f, "etherscan")?;
180            }
181            Self::Sourcify => {
182                write!(f, "sourcify")?;
183            }
184            Self::Blockscout => {
185                write!(f, "blockscout")?;
186            }
187            Self::Oklink => {
188                write!(f, "oklink")?;
189            }
190            Self::Custom => {
191                write!(f, "custom")?;
192            }
193        };
194        Ok(())
195    }
196}
197
198#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
199pub enum VerificationProviderType {
200    Etherscan,
201    #[default]
202    Sourcify,
203    Blockscout,
204    Oklink,
205    /// Custom verification provider, requires compatibility with the Etherscan API.
206    Custom,
207}
208
209impl VerificationProviderType {
210    /// Returns the corresponding `VerificationProvider` for the key.
211    ///
212    /// `is_explicit` should be `true` when the user explicitly passed `--verifier`; `false` when
213    /// the value is the default (Sourcify). An explicit flag always takes precedence over the
214    /// `ETHERSCAN_API_KEY` environment variable.
215    pub fn client(
216        &self,
217        key: Option<&str>,
218        chain: Option<Chain>,
219        has_url: bool,
220        is_explicit: bool,
221    ) -> Result<Box<dyn VerificationProvider>> {
222        let has_key = key.is_some_and(|k| !k.is_empty());
223
224        // 1. Explicit `--verifier sourcify` always wins over ETHERSCAN_API_KEY.
225        if is_explicit && self.is_sourcify() {
226            return Ok(Box::<SourcifyVerificationProvider>::default());
227        }
228
229        // 2. `--verifier etherscan` (explicit): check chain support and require key.
230        if self.is_etherscan() {
231            if let Some(chain) = chain
232                && (chain.etherscan_urls().is_none() || chain.is_custom_sourcify())
233                && !has_url
234            {
235                eyre::bail!(EtherscanConfigError::UnknownChain(
236                    "when using Etherscan verifier".to_string(),
237                    chain
238                ));
239            }
240            if !has_key {
241                eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier");
242            }
243            return Ok(Box::<EtherscanVerificationProvider>::default());
244        }
245
246        // 3. Explicit `--verifier blockscout | oklink | custom`: require a URL.
247        if is_explicit && matches!(self, Self::Blockscout | Self::Oklink | Self::Custom) {
248            if !has_url {
249                eyre::bail!("No verifier URL specified for verifier {}", self);
250            }
251            return Ok(Box::<EtherscanVerificationProvider>::default());
252        }
253
254        // 4. No explicit `--verifier` but ETHERSCAN_API_KEY is set: prefer Etherscan when the chain
255        //    is supported; otherwise warn and fall through to the Sourcify default. See <https://github.com/foundry-rs/foundry/issues/10774>.
256        //    Custom-Sourcify chains (e.g. Tempo) register Sourcify-compatible URLs under
257        //    etherscan_urls() and are hard-excluded here regardless of whether a URL is present.
258        if has_key {
259            if let Some(chain) = chain
260                && (chain.is_custom_sourcify() || chain.etherscan_urls().is_none() && !has_url)
261            {
262                if chain.is_custom_sourcify() {
263                    sh_warn!(
264                        "ETHERSCAN_API_KEY is set but chain {chain} uses a Sourcify-compatible \
265                         API. Falling back to Sourcify. Pass `--verifier sourcify` to suppress \
266                         this warning."
267                    )?;
268                } else {
269                    sh_warn!(
270                        "ETHERSCAN_API_KEY is set but chain {chain} has no known Etherscan API \
271                         URL. Falling back to Sourcify. Pass --verifier-url <URL> or \
272                         `--verifier <provider>` to override."
273                    )?;
274                }
275                // Fall through to branch 5 (Sourcify default) below.
276            } else {
277                return Ok(Box::<EtherscanVerificationProvider>::default());
278            }
279        }
280
281        // 5. No key, no explicit verifier: default to Sourcify.
282        if self.is_sourcify() {
283            return Ok(Box::<SourcifyVerificationProvider>::default());
284        }
285
286        // 6. No valid provider.
287        eyre::bail!(
288            "No valid verification provider specified. Pass the --verifier flag to specify a provider or set the ETHERSCAN_API_KEY environment variable to use Etherscan as a verifier."
289        );
290    }
291
292    pub const fn is_sourcify(&self) -> bool {
293        matches!(self, Self::Sourcify)
294    }
295
296    pub const fn is_etherscan(&self) -> bool {
297        matches!(self, Self::Etherscan)
298    }
299
300    pub const fn is_custom(&self) -> bool {
301        matches!(self, Self::Custom)
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn etherscan_allows_unknown_chain_with_verifier_url() {
311        let chain = Chain::from(3658348u64);
312        let provider = VerificationProviderType::Etherscan
313            .client(Some("key"), Some(chain), true, true)
314            .unwrap();
315        assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
316    }
317
318    #[test]
319    fn etherscan_rejects_unknown_chain_without_verifier_url() {
320        let chain = Chain::from(3658348u64);
321        let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), false, true);
322        match res {
323            Ok(_) => panic!("expected unknown-chain error"),
324            Err(err) => {
325                assert!(err.to_string().contains("No known Etherscan API URL"));
326            }
327        }
328    }
329
330    // Regression: explicit --verifier etherscan on a custom-Sourcify chain (e.g. Tempo) without
331    // --verifier-url must be rejected even though etherscan_urls() returns Some for the chain.
332    #[test]
333    fn explicit_etherscan_on_custom_sourcify_chain_without_url_bails() {
334        let tempo = Chain::from(4217u64); // NamedChain::Tempo
335        let res = VerificationProviderType::Etherscan.client(Some("key"), Some(tempo), false, true);
336        assert!(res.is_err(), "expected error for Etherscan on custom-Sourcify chain w/o URL");
337    }
338
339    // Custom-Sourcify chain with an explicit --verifier-url is allowed for Etherscan.
340    #[test]
341    fn explicit_etherscan_on_custom_sourcify_chain_with_url_is_ok() {
342        let tempo = Chain::from(4217u64);
343        let provider = VerificationProviderType::Etherscan
344            .client(Some("key"), Some(tempo), true, true)
345            .unwrap();
346        assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
347    }
348
349    // Implicit ETHERSCAN_API_KEY on a supported chain selects Etherscan; on a custom-Sourcify
350    // chain it must fall back to Sourcify regardless of whether a --verifier-url is present.
351    #[test]
352    fn implicit_etherscan_custom_sourcify_chain_falls_back_to_sourcify() {
353        // Baseline: implicit key on a normal chain -> Etherscan.
354        let provider = VerificationProviderType::Sourcify
355            .client(Some("mykey"), Some(Chain::mainnet()), false, false)
356            .unwrap();
357        assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
358
359        // Custom-Sourcify chain without URL -> Sourcify.
360        let provider = VerificationProviderType::Sourcify
361            .client(Some("mykey"), Some(Chain::from(4217u64)), false, false)
362            .expect("expected fallback to Sourcify, got error");
363        assert_eq!(provider.provider_type(), VerificationProviderType::Sourcify);
364
365        // Custom-Sourcify chain with URL -> still Sourcify (URL does not override the exclusion).
366        let provider = VerificationProviderType::Sourcify
367            .client(Some("mykey"), Some(Chain::from(4217u64)), true, false)
368            .expect("expected fallback to Sourcify, got error");
369        assert_eq!(provider.provider_type(), VerificationProviderType::Sourcify);
370    }
371
372    // Regression test for <https://github.com/foundry-rs/foundry/issues/10774>:
373    // when --verifier is not set, ETHERSCAN_API_KEY is set, but the chain has no known
374    // Etherscan API URL, `client()` must NOT bail; it should warn and fall back to Sourcify.
375    // (Behavior is verified more strictly via `VerifierArgs::resolve` tests in `verify.rs`.)
376    #[test]
377    fn implicit_etherscan_unknown_chain_falls_back_to_sourcify() {
378        // Baseline: implicit key on a normal chain -> Etherscan.
379        let provider = VerificationProviderType::Sourcify
380            .client(Some("mykey"), Some(Chain::mainnet()), false, false)
381            .unwrap();
382        assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
383
384        // Unknown chain: same call must fall back to Sourcify, not bail.
385        let provider = VerificationProviderType::Sourcify
386            .client(Some("mykey"), Some(Chain::from(3658348u64)), false, false)
387            .expect("expected fallback to Sourcify, got error");
388        assert_eq!(provider.provider_type(), VerificationProviderType::Sourcify);
389    }
390}