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