1use crate::{
4 RetryArgs,
5 etherscan::EtherscanVerificationProvider,
6 provider::{VerificationContext, VerificationProvider, VerificationProviderType},
7 utils::wrap_verifier_url_error,
8};
9use alloy_primitives::{Address, TxHash, map::HashSet};
10use alloy_provider::Provider;
11use clap::{Parser, ValueEnum, ValueHint};
12use eyre::Result;
13use foundry_cli::{
14 opts::{EtherscanOpts, RpcOpts},
15 utils::{self, LoadConfig},
16};
17use foundry_common::{ContractsByArtifact, compile::ProjectCompiler};
18use foundry_compilers::{artifacts::EvmVersion, compilers::solc::Solc, info::ContractInfo};
19use foundry_config::{
20 Chain, Config, SolcReq, figment, impl_figment_convert, impl_figment_convert_cast,
21};
22use itertools::Itertools;
23use semver::BuildMetadata;
24use std::path::PathBuf;
25
26#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
30pub enum ContractLanguage {
31 Solidity,
33 Vyper,
35}
36
37#[derive(Clone, Debug, Parser)]
39pub struct VerifierArgs {
40 #[arg(long, help_heading = "Verifier options", default_value = "sourcify", value_enum)]
42 pub verifier: VerificationProviderType,
43
44 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_KEY")]
46 pub verifier_api_key: Option<String>,
47
48 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")]
50 pub verifier_url: Option<String>,
51}
52
53impl Default for VerifierArgs {
54 fn default() -> Self {
55 Self {
56 verifier: VerificationProviderType::Sourcify,
57 verifier_api_key: None,
58 verifier_url: None,
59 }
60 }
61}
62
63#[derive(Clone, Debug, Parser)]
65pub struct VerifyArgs {
66 pub address: Address,
68
69 pub contract: Option<ContractInfo>,
71
72 #[arg(
74 long,
75 conflicts_with = "constructor_args_path",
76 value_name = "ARGS",
77 visible_alias = "encoded-constructor-args"
78 )]
79 pub constructor_args: Option<String>,
80
81 #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
83 pub constructor_args_path: Option<PathBuf>,
84
85 #[arg(long)]
87 pub guess_constructor_args: bool,
88
89 #[arg(long)]
91 pub creation_transaction_hash: Option<TxHash>,
92
93 #[arg(long, value_name = "VERSION")]
95 pub compiler_version: Option<String>,
96
97 #[arg(long, value_name = "PROFILE_NAME")]
99 pub compilation_profile: Option<String>,
100
101 #[arg(long, visible_alias = "optimizer-runs", value_name = "NUM")]
103 pub num_of_optimizations: Option<usize>,
104
105 #[arg(long)]
107 pub flatten: bool,
108
109 #[arg(short, long)]
111 pub force: bool,
112
113 #[arg(long)]
115 pub skip_is_verified_check: bool,
116
117 #[arg(long)]
119 pub watch: bool,
120
121 #[arg(long, help_heading = "Linker options", env = "DAPP_LIBRARIES")]
123 pub libraries: Vec<String>,
124
125 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
130 pub root: Option<PathBuf>,
131
132 #[arg(long, conflicts_with = "flatten")]
137 pub show_standard_json_input: bool,
138
139 #[arg(long)]
141 pub via_ir: bool,
142
143 #[arg(long)]
147 pub evm_version: Option<EvmVersion>,
148
149 #[arg(long, help_heading = "Compiler options")]
151 pub no_auto_detect: bool,
152
153 #[arg(long = "use", help_heading = "Compiler options", value_name = "SOLC_VERSION")]
157 pub use_solc: Option<String>,
158
159 #[command(flatten)]
160 pub etherscan: EtherscanOpts,
161
162 #[command(flatten)]
163 pub rpc: RpcOpts,
164
165 #[command(flatten)]
166 pub retry: RetryArgs,
167
168 #[command(flatten)]
169 pub verifier: VerifierArgs,
170
171 #[arg(long, value_enum)]
175 pub language: Option<ContractLanguage>,
176}
177
178impl_figment_convert!(VerifyArgs);
179
180impl figment::Provider for VerifyArgs {
181 fn metadata(&self) -> figment::Metadata {
182 figment::Metadata::named("Verify Provider")
183 }
184
185 fn data(
186 &self,
187 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
188 let mut dict = self.etherscan.dict();
189 dict.extend(self.rpc.dict());
190
191 if let Some(root) = self.root.as_ref() {
192 dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
193 }
194 if let Some(optimizer_runs) = self.num_of_optimizations {
195 dict.insert("optimizer".to_string(), figment::value::Value::serialize(true)?);
196 dict.insert(
197 "optimizer_runs".to_string(),
198 figment::value::Value::serialize(optimizer_runs)?,
199 );
200 }
201 if let Some(evm_version) = self.evm_version {
202 dict.insert("evm_version".to_string(), figment::value::Value::serialize(evm_version)?);
203 }
204 if self.via_ir {
205 dict.insert("via_ir".to_string(), figment::value::Value::serialize(self.via_ir)?);
206 }
207
208 if self.no_auto_detect {
209 dict.insert("auto_detect_solc".to_string(), figment::value::Value::serialize(false)?);
210 }
211
212 if let Some(ref solc) = self.use_solc {
213 let solc = solc.trim_start_matches("solc:");
214 dict.insert("solc".to_string(), figment::value::Value::serialize(solc)?);
215 }
216
217 if let Some(api_key) = &self.verifier.verifier_api_key {
218 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
219 }
220
221 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
222 }
223}
224
225impl VerifyArgs {
226 pub async fn run(mut self) -> Result<()> {
228 let config = self.load_config()?;
229
230 if self.guess_constructor_args && config.get_rpc_url().is_none() {
231 eyre::bail!(
232 "You have to provide a valid RPC URL to use --guess-constructor-args feature"
233 )
234 }
235
236 let chain = match config.get_rpc_url() {
239 Some(_) => {
240 let provider = utils::get_provider(&config)?;
241 utils::get_chain(config.chain, provider).await?
242 }
243 None => config.chain.unwrap_or_default(),
244 };
245
246 let context = self.resolve_context().await?;
247
248 self.etherscan.chain = Some(chain);
250 self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
251
252 if self.verifier.verifier.is_sourcify()
254 && self.verifier.verifier_url.is_none()
255 && let Some(url) = sourcify_api_url(chain)
256 {
257 self.verifier.verifier_url = Some(url);
258 }
259
260 if self.show_standard_json_input {
261 let args = EtherscanVerificationProvider::default()
262 .create_verify_request(&self, &context)
263 .await?;
264 sh_println!("{}", args.source)?;
265 return Ok(());
266 }
267
268 let verifier_url = self.verifier.verifier_url.clone();
269 sh_println!("Start verifying contract `{}` deployed on {chain}", self.address)?;
270 if let Some(version) = &self.evm_version {
271 sh_println!("EVM version: {version}")?;
272 }
273 if let Some(version) = &self.compiler_version {
274 sh_println!("Compiler version: {version}")?;
275 }
276 if let Some(optimizations) = &self.num_of_optimizations {
277 sh_println!("Optimizations: {optimizations}")?
278 }
279 if let Some(args) = &self.constructor_args
280 && !args.is_empty()
281 {
282 sh_println!("Constructor args: {args}")?
283 }
284 let etherscan_key = self.etherscan.key();
288 let using_etherscan = self.verifier.verifier.is_etherscan()
289 || (etherscan_key.as_deref().is_some_and(|k| !k.is_empty())
290 && !matches!(
291 self.verifier.verifier,
292 VerificationProviderType::Blockscout
293 | VerificationProviderType::Oklink
294 | VerificationProviderType::Custom
295 ));
296 self.verifier
297 .verifier
298 .client(
299 etherscan_key.as_deref(),
300 self.etherscan.chain,
301 self.verifier.verifier_url.is_some(),
302 )?
303 .verify(self, context)
304 .await
305 .map_err(|err| wrap_verifier_url_error(err, verifier_url.as_deref(), using_etherscan))
306 }
307
308 pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
310 self.verifier.verifier.client(
311 self.etherscan.key().as_deref(),
312 self.etherscan.chain,
313 self.verifier.verifier_url.is_some(),
314 )
315 }
316
317 pub async fn resolve_context(&self) -> Result<VerificationContext> {
320 let mut config = self.load_config()?;
321 config.libraries.extend(self.libraries.clone());
322
323 let project = config.project()?;
324
325 if let Some(ref contract) = self.contract {
326 let contract_path = if let Some(ref path) = contract.path {
327 project.root().join(PathBuf::from(path))
328 } else {
329 project.find_contract_path(&contract.name)?
330 };
331
332 let cache = project.read_cache_file().ok();
333
334 let mut version = if let Some(ref version) = self.compiler_version {
335 version.trim_start_matches('v').parse()?
336 } else if let Some(ref solc) = config.solc {
337 match solc {
338 SolcReq::Version(version) => version.to_owned(),
339 SolcReq::Local(solc) => Solc::new(solc)?.version,
340 }
341 } else if let Some(entry) =
342 cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
343 {
344 let unique_versions = entry
345 .artifacts
346 .get(&contract.name)
347 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
348 .unwrap_or_default();
349
350 if unique_versions.is_empty() {
351 eyre::bail!(
352 "No matching artifact found for {}. This could be due to:\n\
353 - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
354 contract.name
355 );
356 } else if unique_versions.len() > 1 {
357 warn!(
358 "Ambiguous compiler versions found in cache: {}",
359 unique_versions.iter().join(", ")
360 );
361 eyre::bail!(
362 "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
363 )
364 }
365
366 unique_versions.into_iter().next().unwrap().to_owned()
367 } else {
368 eyre::bail!(
369 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
370 )
371 };
372
373 let settings = if let Some(profile) = &self.compilation_profile {
374 if profile == "default" {
375 &project.settings
376 } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
377 settings
378 } else {
379 eyre::bail!("Unknown compilation profile: {}", profile)
380 }
381 } else if let Some((cache, entry)) = cache
382 .as_ref()
383 .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
384 {
385 let profiles = entry
386 .artifacts
387 .get(&contract.name)
388 .and_then(|artifacts| {
389 let mut cached_artifacts = artifacts.get(&version);
390 if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
398 version.build = BuildMetadata::EMPTY;
399 cached_artifacts = artifacts.get(&version);
400 }
401 cached_artifacts
402 })
403 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
404 .unwrap_or_default();
405
406 if profiles.is_empty() {
407 eyre::bail!(
408 "No matching artifact found for {} with compiler version {}. This could be due to:\n\
409 - Compiler version mismatch - the contract was compiled with a different Solidity version",
410 contract.name,
411 version
412 );
413 } else if profiles.len() > 1 {
414 eyre::bail!(
415 "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
416 profiles.iter().join(", ")
417 )
418 }
419
420 let profile = profiles.into_iter().next().unwrap().to_owned();
421 cache.profiles.get(&profile).expect("must be present")
422 } else if project.additional_settings.is_empty() {
423 &project.settings
424 } else {
425 eyre::bail!(
426 "If cache is disabled, compilation profile must be provided with `--compilation-profile` option or set in foundry.toml"
427 )
428 };
429
430 VerificationContext::new(
431 contract_path,
432 contract.name.clone(),
433 version,
434 config,
435 settings.clone(),
436 )
437 } else {
438 if config.get_rpc_url().is_none() {
439 eyre::bail!("You have to provide a contract name or a valid RPC URL")
440 }
441 let provider = utils::get_provider(&config)?;
442 let code = provider.get_code_at(self.address).await?;
443
444 let output = ProjectCompiler::new().compile(&project)?;
445 let contracts = ContractsByArtifact::new(
446 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
447 );
448
449 let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
450 eyre::bail!(format!(
451 "Bytecode at {} does not match any local contracts",
452 self.address
453 ))
454 };
455
456 let settings = project
457 .settings_profiles()
458 .find_map(|(name, settings)| {
459 (name == artifact_id.profile.as_str()).then_some(settings)
460 })
461 .expect("must be present");
462
463 VerificationContext::new(
464 artifact_id.source.clone(),
465 artifact_id.name.split('.').next().unwrap().to_owned(),
466 artifact_id.version.clone(),
467 config,
468 settings.clone(),
469 )
470 }
471 }
472
473 pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
475 self.language.unwrap_or_else(|| {
476 match ctx.target_path.extension().and_then(|e| e.to_str()) {
477 Some("vy") => ContractLanguage::Vyper,
478 _ => ContractLanguage::Solidity,
479 }
480 })
481 }
482}
483
484#[derive(Clone, Debug, Parser)]
486pub struct VerifyCheckArgs {
487 pub id: String,
493
494 #[command(flatten)]
495 pub retry: RetryArgs,
496
497 #[command(flatten)]
498 pub etherscan: EtherscanOpts,
499
500 #[command(flatten)]
501 pub verifier: VerifierArgs,
502}
503
504impl_figment_convert_cast!(VerifyCheckArgs);
505
506impl VerifyCheckArgs {
507 pub async fn run(self) -> Result<()> {
509 sh_println!(
510 "Checking verification status on {}",
511 self.etherscan.chain.unwrap_or_default()
512 )?;
513 self.verifier
514 .verifier
515 .client(
516 self.etherscan.key().as_deref(),
517 self.etherscan.chain,
518 self.verifier.verifier_url.is_some(),
519 )?
520 .check(self)
521 .await
522 }
523}
524
525impl figment::Provider for VerifyCheckArgs {
526 fn metadata(&self) -> figment::Metadata {
527 figment::Metadata::named("Verify Check Provider")
528 }
529
530 fn data(
531 &self,
532 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
533 let mut dict = self.etherscan.dict();
534 if let Some(api_key) = &self.etherscan.key {
535 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
536 }
537
538 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
539 }
540}
541
542fn sourcify_api_url(chain: Chain) -> Option<String> {
547 if chain.is_custom_sourcify() {
548 chain.etherscan_urls().map(|(api_url, _)| {
549 let api_url = api_url.trim_end_matches('/');
550 format!("{api_url}/")
551 })
552 } else {
553 None
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn can_parse_verify_contract() {
563 let args: VerifyArgs = VerifyArgs::parse_from([
564 "foundry-cli",
565 "0x0000000000000000000000000000000000000000",
566 "src/Domains.sol:Domains",
567 "--via-ir",
568 ]);
569 assert!(args.via_ir);
570 }
571
572 #[test]
573 fn can_parse_new_compiler_flags() {
574 let args: VerifyArgs = VerifyArgs::parse_from([
575 "foundry-cli",
576 "0x0000000000000000000000000000000000000000",
577 "src/Domains.sol:Domains",
578 "--no-auto-detect",
579 "--use",
580 "0.8.23",
581 ]);
582 assert!(args.no_auto_detect);
583 assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
584 }
585}