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