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