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) => {
289 if is_host_only(&url) {
290 return err.wrap_err(format!(
291 "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
292 ))
293 }
294 }
295 Err(url_err) => {
296 return err.wrap_err(format!(
297 "Invalid URL {verifier_url} provided: {url_err}"
298 ))
299 }
300 }
301 }
302
303 err
304 })
305 }
306
307 pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
309 self.verifier.verifier.client(
310 self.etherscan.key().as_deref(),
311 self.etherscan.chain,
312 self.verifier.verifier_url.is_some(),
313 )
314 }
315
316 pub async fn resolve_context(&self) -> Result<VerificationContext> {
319 let mut config = self.load_config()?;
320 config.libraries.extend(self.libraries.clone());
321
322 let project = config.project()?;
323
324 if let Some(ref contract) = self.contract {
325 let contract_path = if let Some(ref path) = contract.path {
326 project.root().join(PathBuf::from(path))
327 } else {
328 project.find_contract_path(&contract.name)?
329 };
330
331 let cache = project.read_cache_file().ok();
332
333 let mut version = if let Some(ref version) = self.compiler_version {
334 version.trim_start_matches('v').parse()?
335 } else if let Some(ref solc) = config.solc {
336 match solc {
337 SolcReq::Version(version) => version.to_owned(),
338 SolcReq::Local(solc) => Solc::new(solc)?.version,
339 }
340 } else if let Some(entry) =
341 cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
342 {
343 let unique_versions = entry
344 .artifacts
345 .get(&contract.name)
346 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
347 .unwrap_or_default();
348
349 if unique_versions.is_empty() {
350 eyre::bail!(
351 "No matching artifact found for {}. This could be due to:\n\
352 - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
353 contract.name
354 );
355 } else if unique_versions.len() > 1 {
356 warn!(
357 "Ambiguous compiler versions found in cache: {}",
358 unique_versions.iter().join(", ")
359 );
360 eyre::bail!(
361 "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
362 )
363 }
364
365 unique_versions.into_iter().next().unwrap().to_owned()
366 } else {
367 eyre::bail!(
368 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
369 )
370 };
371
372 let settings = if let Some(profile) = &self.compilation_profile {
373 if profile == "default" {
374 &project.settings
375 } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
376 settings
377 } else {
378 eyre::bail!("Unknown compilation profile: {}", profile)
379 }
380 } else if let Some((cache, entry)) = cache
381 .as_ref()
382 .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
383 {
384 let profiles = entry
385 .artifacts
386 .get(&contract.name)
387 .and_then(|artifacts| {
388 let mut cached_artifacts = artifacts.get(&version);
389 if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
397 version.build = BuildMetadata::EMPTY;
398 cached_artifacts = artifacts.get(&version);
399 }
400 cached_artifacts
401 })
402 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
403 .unwrap_or_default();
404
405 if profiles.is_empty() {
406 eyre::bail!(
407 "No matching artifact found for {} with compiler version {}. This could be due to:\n\
408 - Compiler version mismatch - the contract was compiled with a different Solidity version",
409 contract.name,
410 version
411 );
412 } else if profiles.len() > 1 {
413 eyre::bail!(
414 "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
415 profiles.iter().join(", ")
416 )
417 }
418
419 let profile = profiles.into_iter().next().unwrap().to_owned();
420 cache.profiles.get(&profile).expect("must be present")
421 } else if project.additional_settings.is_empty() {
422 &project.settings
423 } else {
424 eyre::bail!(
425 "If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml"
426 )
427 };
428
429 VerificationContext::new(
430 contract_path,
431 contract.name.clone(),
432 version,
433 config,
434 settings.clone(),
435 )
436 } else {
437 if config.get_rpc_url().is_none() {
438 eyre::bail!("You have to provide a contract name or a valid RPC URL")
439 }
440 let provider = utils::get_provider(&config)?;
441 let code = provider.get_code_at(self.address).await?;
442
443 let output = ProjectCompiler::new().compile(&project)?;
444 let contracts = ContractsByArtifact::new(
445 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
446 );
447
448 let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
449 eyre::bail!(format!(
450 "Bytecode at {} does not match any local contracts",
451 self.address
452 ))
453 };
454
455 let settings = project
456 .settings_profiles()
457 .find_map(|(name, settings)| {
458 (name == artifact_id.profile.as_str()).then_some(settings)
459 })
460 .expect("must be present");
461
462 VerificationContext::new(
463 artifact_id.source.clone(),
464 artifact_id.name.split('.').next().unwrap().to_owned(),
465 artifact_id.version.clone(),
466 config,
467 settings.clone(),
468 )
469 }
470 }
471
472 pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
474 self.language.unwrap_or_else(|| {
475 match ctx.target_path.extension().and_then(|e| e.to_str()) {
476 Some("vy") => ContractLanguage::Vyper,
477 _ => ContractLanguage::Solidity,
478 }
479 })
480 }
481}
482
483#[derive(Clone, Debug, Parser)]
485pub struct VerifyCheckArgs {
486 pub id: String,
492
493 #[command(flatten)]
494 pub retry: RetryArgs,
495
496 #[command(flatten)]
497 pub etherscan: EtherscanOpts,
498
499 #[command(flatten)]
500 pub verifier: VerifierArgs,
501}
502
503impl_figment_convert_cast!(VerifyCheckArgs);
504
505impl VerifyCheckArgs {
506 pub async fn run(self) -> Result<()> {
508 sh_println!(
509 "Checking verification status on {}",
510 self.etherscan.chain.unwrap_or_default()
511 )?;
512 self.verifier
513 .verifier
514 .client(
515 self.etherscan.key().as_deref(),
516 self.etherscan.chain,
517 self.verifier.verifier_url.is_some(),
518 )?
519 .check(self)
520 .await
521 }
522}
523
524impl figment::Provider for VerifyCheckArgs {
525 fn metadata(&self) -> figment::Metadata {
526 figment::Metadata::named("Verify Check Provider")
527 }
528
529 fn data(
530 &self,
531 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
532 let mut dict = self.etherscan.dict();
533 if let Some(api_key) = &self.etherscan.key {
534 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
535 }
536
537 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
538 }
539}
540
541fn sourcify_api_url(chain: Chain) -> Option<String> {
546 if chain.is_custom_sourcify() {
547 chain.etherscan_urls().map(|(api_url, _)| {
548 let api_url = api_url.trim_end_matches('/');
549 format!("{api_url}/")
550 })
551 } else {
552 None
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn can_parse_verify_contract() {
562 let args: VerifyArgs = VerifyArgs::parse_from([
563 "foundry-cli",
564 "0x0000000000000000000000000000000000000000",
565 "src/Domains.sol:Domains",
566 "--via-ir",
567 ]);
568 assert!(args.via_ir);
569 }
570
571 #[test]
572 fn can_parse_new_compiler_flags() {
573 let args: VerifyArgs = VerifyArgs::parse_from([
574 "foundry-cli",
575 "0x0000000000000000000000000000000000000000",
576 "src/Domains.sol:Domains",
577 "--no-auto-detect",
578 "--use",
579 "0.8.23",
580 ]);
581 assert!(args.no_auto_detect);
582 assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
583 }
584}