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