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#[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 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 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#[async_trait]
109pub trait VerificationProvider {
110 fn provider_type(&self) -> VerificationProviderType;
112
113 async fn preflight_verify_check(
121 &mut self,
122 args: VerifyArgs,
123 context: VerificationContext,
124 ) -> Result<()>;
125
126 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()>;
128
129 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,
180}
181
182impl VerificationProviderType {
183 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 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 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 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 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 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 #[test]
314 fn explicit_etherscan_on_custom_sourcify_chain_without_url_bails() {
315 let tempo = Chain::from(4217u64); 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 #[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 #[test]
333 fn implicit_etherscan_custom_sourcify_chain_falls_back_to_sourcify() {
334 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 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 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 #[test]
358 fn implicit_etherscan_unknown_chain_falls_back_to_sourcify() {
359 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 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}