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#[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 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 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#[async_trait]
94pub trait VerificationProvider {
95 fn provider_type(&self) -> VerificationProviderType;
97
98 async fn preflight_verify_check(
106 &mut self,
107 args: VerifyArgs,
108 context: VerificationContext,
109 ) -> Result<()>;
110
111 async fn submit(
117 &mut self,
118 args: VerifyArgs,
119 context: VerificationContext,
120 ) -> Result<Option<VerifyCheckArgs>>;
121
122 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 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,
184}
185
186impl VerificationProviderType {
187 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 if is_explicit && self.is_sourcify() {
203 return Ok(Box::<SourcifyVerificationProvider>::default());
204 }
205
206 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 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 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 } else {
254 return Ok(Box::<EtherscanVerificationProvider>::default());
255 }
256 }
257
258 if self.is_sourcify() {
260 return Ok(Box::<SourcifyVerificationProvider>::default());
261 }
262
263 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 #[test]
310 fn explicit_etherscan_on_custom_sourcify_chain_without_url_bails() {
311 let tempo = Chain::from(4217u64); 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 #[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 #[test]
329 fn implicit_etherscan_custom_sourcify_chain_falls_back_to_sourcify() {
330 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 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 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 #[test]
354 fn implicit_etherscan_unknown_chain_falls_back_to_sourcify() {
355 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 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}