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::{OptionExt, Result};
9use foundry_common::compile::ProjectCompiler;
10use foundry_compilers::{
11    Project, artifacts::output_selection::OutputSelection, compilers::solc::SolcCompiler,
12    multi::MultiCompilerSettings, solc::Solc,
13};
14use foundry_config::{Chain, Config, EtherscanConfigError};
15use semver::Version;
16use std::{fmt, path::PathBuf, str::FromStr};
17
18/// Container with data required for contract verification.
19#[derive(Debug, Clone)]
20pub struct VerificationContext {
21    pub config: Config,
22    pub project: Project,
23    pub target_path: PathBuf,
24    pub target_name: String,
25    pub compiler_version: Version,
26    pub compiler_settings: MultiCompilerSettings,
27}
28
29impl VerificationContext {
30    pub fn new(
31        target_path: PathBuf,
32        target_name: String,
33        compiler_version: Version,
34        config: Config,
35        compiler_settings: MultiCompilerSettings,
36    ) -> Result<Self> {
37        let mut project = config.project()?;
38        project.no_artifacts = true;
39
40        let solc = Solc::find_or_install(&compiler_version)?;
41        project.compiler.solc = Some(SolcCompiler::Specific(solc));
42
43        Ok(Self { config, project, target_name, target_path, compiler_version, compiler_settings })
44    }
45
46    /// Compiles target contract requesting only ABI and returns it.
47    pub fn get_target_abi(&self) -> Result<JsonAbi> {
48        let mut project = self.project.clone();
49        project.update_output_selection(|selection| {
50            *selection = OutputSelection::common_output_selection(["abi".to_string()]);
51        });
52
53        let output = ProjectCompiler::new()
54            .quiet(true)
55            .files([self.target_path.clone()])
56            .compile(&project)?;
57
58        let artifact = output
59            .find(&self.target_path, &self.target_name)
60            .ok_or_eyre("failed to find target artifact when compiling for abi")?;
61
62        artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
63    }
64}
65
66/// An abstraction for various verification providers such as etherscan, sourcify, blockscout
67#[async_trait]
68pub trait VerificationProvider {
69    /// This should ensure the verify request can be prepared successfully.
70    ///
71    /// Caution: Implementers must ensure that this _never_ sends the actual verify request
72    /// `[VerificationProvider::verify]`, instead this is supposed to evaluate whether the given
73    /// [`VerifyArgs`] are valid to begin with. This should prevent situations where there's a
74    /// contract deployment that's executed before the verify request and the subsequent verify task
75    /// fails due to misconfiguration.
76    async fn preflight_verify_check(
77        &mut self,
78        args: VerifyArgs,
79        context: VerificationContext,
80    ) -> Result<()>;
81
82    /// Sends the actual verify request for the targeted contract.
83    async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()>;
84
85    /// Checks whether the contract is verified.
86    async fn check(&self, args: VerifyCheckArgs) -> Result<()>;
87}
88
89impl FromStr for VerificationProviderType {
90    type Err = String;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        match s {
94            "e" | "etherscan" => Ok(Self::Etherscan),
95            "s" | "sourcify" => Ok(Self::Sourcify),
96            "b" | "blockscout" => Ok(Self::Blockscout),
97            "o" | "oklink" => Ok(Self::Oklink),
98            "c" | "custom" => Ok(Self::Custom),
99            _ => Err(format!("Unknown provider: {s}")),
100        }
101    }
102}
103
104impl fmt::Display for VerificationProviderType {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Self::Etherscan => {
108                write!(f, "etherscan")?;
109            }
110            Self::Sourcify => {
111                write!(f, "sourcify")?;
112            }
113            Self::Blockscout => {
114                write!(f, "blockscout")?;
115            }
116            Self::Oklink => {
117                write!(f, "oklink")?;
118            }
119            Self::Custom => {
120                write!(f, "custom")?;
121            }
122        };
123        Ok(())
124    }
125}
126
127#[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
128pub enum VerificationProviderType {
129    Etherscan,
130    #[default]
131    Sourcify,
132    Blockscout,
133    Oklink,
134    /// Custom verification provider, requires compatibility with the Etherscan API.
135    Custom,
136}
137
138impl VerificationProviderType {
139    /// Returns the corresponding `VerificationProvider` for the key
140    pub fn client(
141        &self,
142        key: Option<&str>,
143        chain: Option<Chain>,
144        has_url: bool,
145    ) -> Result<Box<dyn VerificationProvider>> {
146        let has_key = key.as_ref().is_some_and(|k| !k.is_empty());
147        // 1. If no verifier or `--verifier sourcify` is set and no API key provided, use Sourcify.
148        if !has_key && self.is_sourcify() {
149            sh_println!(
150                "Attempting to verify on Sourcify. Pass the --etherscan-api-key <API_KEY> to verify on Etherscan, \
151            or use the --verifier flag to verify on another provider."
152            )?;
153            return Ok(Box::<SourcifyVerificationProvider>::default());
154        }
155
156        // 2. If `--verifier etherscan` is explicitly set, check if chain is supported and
157        // enforce the API key requirement.
158        if self.is_etherscan() {
159            if let Some(chain) = chain
160                && chain.etherscan_urls().is_none()
161                && !has_url
162            {
163                eyre::bail!(EtherscanConfigError::UnknownChain(
164                    "when using Etherscan verifier".to_string(),
165                    chain
166                ))
167            }
168            if !has_key {
169                eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier")
170            }
171            return Ok(Box::<EtherscanVerificationProvider>::default());
172        }
173
174        // 3. If `--verifier blockscout | oklink | custom` is explicitly set, use the chosen
175        //    verifier and make sure an URL was specified.
176        if matches!(self, Self::Blockscout | Self::Oklink | Self::Custom) {
177            if !has_url {
178                eyre::bail!("No verifier URL specified for verifier {}", self);
179            }
180            return Ok(Box::<EtherscanVerificationProvider>::default());
181        }
182
183        // 4. If no `--verifier` is specified but `ETHERSCAN_API_KEY` is set, default to Etherscan.
184        if has_key {
185            return Ok(Box::<EtherscanVerificationProvider>::default());
186        }
187
188        // 5. If no valid provider is specified, bail.
189        eyre::bail!(
190            "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."
191        )
192    }
193
194    pub fn is_sourcify(&self) -> bool {
195        matches!(self, Self::Sourcify)
196    }
197
198    pub fn is_etherscan(&self) -> bool {
199        matches!(self, Self::Etherscan)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn etherscan_allows_unknown_chain_with_verifier_url() {
209        let chain = Chain::from(3658348u64);
210        let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), true);
211        assert!(res.is_ok());
212    }
213
214    #[test]
215    fn etherscan_rejects_unknown_chain_without_verifier_url() {
216        let chain = Chain::from(3658348u64);
217        let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), false);
218        match res {
219            Ok(_) => panic!("expected unknown-chain error"),
220            Err(err) => {
221                assert!(err.to_string().contains("No known Etherscan API URL"));
222            }
223        }
224    }
225}