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    /// 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    /// Sends the actual verify request for the targeted contract.
124    async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()>;
125
126    /// Checks whether the contract is verified.
127    async fn check(&self, args: VerifyCheckArgs) -> Result<()>;
128}
129
130impl FromStr for VerificationProviderType {
131    type Err = String;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        match s {
135            "e" | "etherscan" => Ok(Self::Etherscan),
136            "s" | "sourcify" => Ok(Self::Sourcify),
137            "b" | "blockscout" => Ok(Self::Blockscout),
138            "o" | "oklink" => Ok(Self::Oklink),
139            "c" | "custom" => Ok(Self::Custom),
140            _ => Err(format!("Unknown provider: {s}")),
141        }
142    }
143}
144
145impl fmt::Display for VerificationProviderType {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::Etherscan => {
149                write!(f, "etherscan")?;
150            }
151            Self::Sourcify => {
152                write!(f, "sourcify")?;
153            }
154            Self::Blockscout => {
155                write!(f, "blockscout")?;
156            }
157            Self::Oklink => {
158                write!(f, "oklink")?;
159            }
160            Self::Custom => {
161                write!(f, "custom")?;
162            }
163        };
164        Ok(())
165    }
166}
167
168#[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
169pub enum VerificationProviderType {
170    Etherscan,
171    #[default]
172    Sourcify,
173    Blockscout,
174    Oklink,
175    /// Custom verification provider, requires compatibility with the Etherscan API.
176    Custom,
177}
178
179impl VerificationProviderType {
180    /// Returns the corresponding `VerificationProvider` for the key
181    pub fn client(
182        &self,
183        key: Option<&str>,
184        chain: Option<Chain>,
185        has_url: bool,
186    ) -> Result<Box<dyn VerificationProvider>> {
187        let has_key = key.as_ref().is_some_and(|k| !k.is_empty());
188        // 1. If no verifier or `--verifier sourcify` is set and no API key provided, use Sourcify.
189        if !has_key && self.is_sourcify() {
190            sh_println!(
191                "Attempting to verify on Sourcify. Pass the --etherscan-api-key <API_KEY> to verify on Etherscan, \
192            or use the --verifier flag to verify on another provider."
193            )?;
194            return Ok(Box::<SourcifyVerificationProvider>::default());
195        }
196
197        // 2. If `--verifier etherscan` is explicitly set, check if chain is supported and
198        // enforce the API key requirement.
199        if self.is_etherscan() {
200            if let Some(chain) = chain
201                && chain.etherscan_urls().is_none()
202                && !has_url
203            {
204                eyre::bail!(EtherscanConfigError::UnknownChain(
205                    "when using Etherscan verifier".to_string(),
206                    chain
207                ))
208            }
209            if !has_key {
210                eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier")
211            }
212            return Ok(Box::<EtherscanVerificationProvider>::default());
213        }
214
215        // 3. If `--verifier blockscout | oklink | custom` is explicitly set, use the chosen
216        //    verifier and make sure an URL was specified.
217        if matches!(self, Self::Blockscout | Self::Oklink | Self::Custom) {
218            if !has_url {
219                eyre::bail!("No verifier URL specified for verifier {}", self);
220            }
221            return Ok(Box::<EtherscanVerificationProvider>::default());
222        }
223
224        // 4. If no `--verifier` is specified but `ETHERSCAN_API_KEY` is set, default to Etherscan.
225        if has_key {
226            return Ok(Box::<EtherscanVerificationProvider>::default());
227        }
228
229        // 5. If no valid provider is specified, bail.
230        eyre::bail!(
231            "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."
232        )
233    }
234
235    pub fn is_sourcify(&self) -> bool {
236        matches!(self, Self::Sourcify)
237    }
238
239    pub fn is_etherscan(&self) -> bool {
240        matches!(self, Self::Etherscan)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn etherscan_allows_unknown_chain_with_verifier_url() {
250        let chain = Chain::from(3658348u64);
251        let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), true);
252        assert!(res.is_ok());
253    }
254
255    #[test]
256    fn etherscan_rejects_unknown_chain_without_verifier_url() {
257        let chain = Chain::from(3658348u64);
258        let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), false);
259        match res {
260            Ok(_) => panic!("expected unknown-chain error"),
261            Err(err) => {
262                assert!(err.to_string().contains("No known Etherscan API URL"));
263            }
264        }
265    }
266}