1use crate::{
4 RetryArgs,
5 etherscan::EtherscanVerificationProvider,
6 provider::{VerificationContext, VerificationProvider, VerificationProviderType},
7 utils::is_host_only,
8};
9use alloy_primitives::{Address, map::HashSet};
10use alloy_provider::Provider;
11use clap::{Parser, ValueEnum, ValueHint};
12use eyre::Result;
13use foundry_block_explorers::EtherscanApiVersion;
14use foundry_cli::{
15 opts::{EtherscanOpts, RpcOpts},
16 utils::{self, LoadConfig},
17};
18use foundry_common::{ContractsByArtifact, compile::ProjectCompiler};
19use foundry_compilers::{artifacts::EvmVersion, compilers::solc::Solc, info::ContractInfo};
20use foundry_config::{Config, SolcReq, figment, impl_figment_convert, impl_figment_convert_cast};
21use itertools::Itertools;
22use reqwest::Url;
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 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_VERSION")]
54 pub verifier_api_version: Option<EtherscanApiVersion>,
55}
56
57impl Default for VerifierArgs {
58 fn default() -> Self {
59 Self {
60 verifier: VerificationProviderType::Sourcify,
61 verifier_api_key: None,
62 verifier_url: None,
63 verifier_api_version: None,
64 }
65 }
66}
67
68#[derive(Clone, Debug, Parser)]
70pub struct VerifyArgs {
71 pub address: Address,
73
74 pub contract: Option<ContractInfo>,
76
77 #[arg(
79 long,
80 conflicts_with = "constructor_args_path",
81 value_name = "ARGS",
82 visible_alias = "encoded-constructor-args"
83 )]
84 pub constructor_args: Option<String>,
85
86 #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
88 pub constructor_args_path: Option<PathBuf>,
89
90 #[arg(long)]
92 pub guess_constructor_args: bool,
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 #[command(flatten)]
151 pub etherscan: EtherscanOpts,
152
153 #[command(flatten)]
154 pub rpc: RpcOpts,
155
156 #[command(flatten)]
157 pub retry: RetryArgs,
158
159 #[command(flatten)]
160 pub verifier: VerifierArgs,
161
162 #[arg(long, value_enum)]
166 pub language: Option<ContractLanguage>,
167}
168
169impl_figment_convert!(VerifyArgs);
170
171impl figment::Provider for VerifyArgs {
172 fn metadata(&self) -> figment::Metadata {
173 figment::Metadata::named("Verify Provider")
174 }
175
176 fn data(
177 &self,
178 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
179 let mut dict = self.etherscan.dict();
180 dict.extend(self.rpc.dict());
181
182 if let Some(root) = self.root.as_ref() {
183 dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
184 }
185 if let Some(optimizer_runs) = self.num_of_optimizations {
186 dict.insert("optimizer".to_string(), figment::value::Value::serialize(true)?);
187 dict.insert(
188 "optimizer_runs".to_string(),
189 figment::value::Value::serialize(optimizer_runs)?,
190 );
191 }
192 if let Some(evm_version) = self.evm_version {
193 dict.insert("evm_version".to_string(), figment::value::Value::serialize(evm_version)?);
194 }
195 if self.via_ir {
196 dict.insert("via_ir".to_string(), figment::value::Value::serialize(self.via_ir)?);
197 }
198
199 if let Some(api_key) = &self.verifier.verifier_api_key {
200 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
201 }
202
203 if let Some(api_version) = &self.verifier.verifier_api_version {
204 dict.insert("etherscan_api_version".into(), api_version.to_string().into());
205 }
206
207 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
208 }
209}
210
211impl VerifyArgs {
212 pub async fn run(mut self) -> Result<()> {
214 let config = self.load_config()?;
215
216 if self.guess_constructor_args && config.get_rpc_url().is_none() {
217 eyre::bail!(
218 "You have to provide a valid RPC URL to use --guess-constructor-args feature"
219 )
220 }
221
222 let chain = match config.get_rpc_url() {
225 Some(_) => {
226 let provider = utils::get_provider(&config)?;
227 utils::get_chain(config.chain, provider).await?
228 }
229 None => config.chain.unwrap_or_default(),
230 };
231
232 let context = self.resolve_context().await?;
233
234 self.etherscan.chain = Some(chain);
236 self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
237
238 if self.show_standard_json_input {
239 let args = EtherscanVerificationProvider::default()
240 .create_verify_request(&self, &context)
241 .await?;
242 sh_println!("{}", args.source)?;
243 return Ok(());
244 }
245
246 let verifier_url = self.verifier.verifier_url.clone();
247 sh_println!("Start verifying contract `{}` deployed on {chain}", self.address)?;
248 if let Some(version) = &self.evm_version {
249 sh_println!("EVM version: {version}")?;
250 }
251 if let Some(version) = &self.compiler_version {
252 sh_println!("Compiler version: {version}")?;
253 }
254 if let Some(optimizations) = &self.num_of_optimizations {
255 sh_println!("Optimizations: {optimizations}")?
256 }
257 if let Some(args) = &self.constructor_args
258 && !args.is_empty()
259 {
260 sh_println!("Constructor args: {args}")?
261 }
262 self.verifier.verifier.client(self.etherscan.key().as_deref())?.verify(self, context).await.map_err(|err| {
263 if let Some(verifier_url) = verifier_url {
264 match Url::parse(&verifier_url) {
265 Ok(url) => {
266 if is_host_only(&url) {
267 return err.wrap_err(format!(
268 "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
269 ))
270 }
271 }
272 Err(url_err) => {
273 return err.wrap_err(format!(
274 "Invalid URL {verifier_url} provided: {url_err}"
275 ))
276 }
277 }
278 }
279
280 err
281 })
282 }
283
284 pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
286 self.verifier.verifier.client(self.etherscan.key().as_deref())
287 }
288
289 pub async fn resolve_context(&self) -> Result<VerificationContext> {
292 let mut config = self.load_config()?;
293 config.libraries.extend(self.libraries.clone());
294
295 let project = config.project()?;
296
297 if let Some(ref contract) = self.contract {
298 let contract_path = if let Some(ref path) = contract.path {
299 project.root().join(PathBuf::from(path))
300 } else {
301 project.find_contract_path(&contract.name)?
302 };
303
304 let cache = project.read_cache_file().ok();
305
306 let mut version = if let Some(ref version) = self.compiler_version {
307 version.trim_start_matches('v').parse()?
308 } else if let Some(ref solc) = config.solc {
309 match solc {
310 SolcReq::Version(version) => version.to_owned(),
311 SolcReq::Local(solc) => Solc::new(solc)?.version,
312 }
313 } else if let Some(entry) =
314 cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
315 {
316 let unique_versions = entry
317 .artifacts
318 .get(&contract.name)
319 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
320 .unwrap_or_default();
321
322 if unique_versions.is_empty() {
323 eyre::bail!("No matching artifact found for {}", contract.name);
324 } else if unique_versions.len() > 1 {
325 warn!(
326 "Ambiguous compiler versions found in cache: {}",
327 unique_versions.iter().join(", ")
328 );
329 eyre::bail!(
330 "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
331 )
332 }
333
334 unique_versions.into_iter().next().unwrap().to_owned()
335 } else {
336 eyre::bail!(
337 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
338 )
339 };
340
341 let settings = if let Some(profile) = &self.compilation_profile {
342 if profile == "default" {
343 &project.settings
344 } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
345 settings
346 } else {
347 eyre::bail!("Unknown compilation profile: {}", profile)
348 }
349 } else if let Some((cache, entry)) = cache
350 .as_ref()
351 .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
352 {
353 let profiles = entry
354 .artifacts
355 .get(&contract.name)
356 .and_then(|artifacts| {
357 let mut cached_artifacts = artifacts.get(&version);
358 if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
366 version.build = BuildMetadata::EMPTY;
367 cached_artifacts = artifacts.get(&version);
368 }
369 cached_artifacts
370 })
371 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
372 .unwrap_or_default();
373
374 if profiles.is_empty() {
375 eyre::bail!("No matching artifact found for {}", contract.name);
376 } else if profiles.len() > 1 {
377 eyre::bail!(
378 "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
379 profiles.iter().join(", ")
380 )
381 }
382
383 let profile = profiles.into_iter().next().unwrap().to_owned();
384 cache.profiles.get(&profile).expect("must be present")
385 } else if project.additional_settings.is_empty() {
386 &project.settings
387 } else {
388 eyre::bail!(
389 "If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml"
390 )
391 };
392
393 VerificationContext::new(
394 contract_path,
395 contract.name.clone(),
396 version,
397 config,
398 settings.clone(),
399 )
400 } else {
401 if config.get_rpc_url().is_none() {
402 eyre::bail!("You have to provide a contract name or a valid RPC URL")
403 }
404 let provider = utils::get_provider(&config)?;
405 let code = provider.get_code_at(self.address).await?;
406
407 let output = ProjectCompiler::new().compile(&project)?;
408 let contracts = ContractsByArtifact::new(
409 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
410 );
411
412 let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
413 eyre::bail!(format!(
414 "Bytecode at {} does not match any local contracts",
415 self.address
416 ))
417 };
418
419 let settings = project
420 .settings_profiles()
421 .find_map(|(name, settings)| {
422 (name == artifact_id.profile.as_str()).then_some(settings)
423 })
424 .expect("must be present");
425
426 VerificationContext::new(
427 artifact_id.source.clone(),
428 artifact_id.name.split('.').next().unwrap().to_owned(),
429 artifact_id.version.clone(),
430 config,
431 settings.clone(),
432 )
433 }
434 }
435
436 pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
438 self.language.unwrap_or_else(|| {
439 match ctx.target_path.extension().and_then(|e| e.to_str()) {
440 Some("vy") => ContractLanguage::Vyper,
441 _ => ContractLanguage::Solidity,
442 }
443 })
444 }
445}
446
447#[derive(Clone, Debug, Parser)]
449pub struct VerifyCheckArgs {
450 pub id: String,
456
457 #[command(flatten)]
458 pub retry: RetryArgs,
459
460 #[command(flatten)]
461 pub etherscan: EtherscanOpts,
462
463 #[command(flatten)]
464 pub verifier: VerifierArgs,
465}
466
467impl_figment_convert_cast!(VerifyCheckArgs);
468
469impl VerifyCheckArgs {
470 pub async fn run(self) -> Result<()> {
472 sh_println!(
473 "Checking verification status on {}",
474 self.etherscan.chain.unwrap_or_default()
475 )?;
476 self.verifier.verifier.client(self.etherscan.key().as_deref())?.check(self).await
477 }
478}
479
480impl figment::Provider for VerifyCheckArgs {
481 fn metadata(&self) -> figment::Metadata {
482 figment::Metadata::named("Verify Check Provider")
483 }
484
485 fn data(
486 &self,
487 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
488 let mut dict = self.etherscan.dict();
489 if let Some(api_key) = &self.etherscan.key {
490 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
491 }
492
493 if let Some(api_version) = &self.etherscan.api_version {
494 dict.insert("etherscan_api_version".into(), api_version.to_string().into());
495 }
496
497 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn can_parse_verify_contract() {
507 let args: VerifyArgs = VerifyArgs::parse_from([
508 "foundry-cli",
509 "0x0000000000000000000000000000000000000000",
510 "src/Domains.sol:Domains",
511 "--via-ir",
512 ]);
513 assert!(args.via_ir);
514 }
515}