1use crate::{
4 etherscan::EtherscanVerificationProvider,
5 provider::{VerificationContext, VerificationProvider, VerificationProviderType},
6 utils::is_host_only,
7 RetryArgs,
8};
9use alloy_primitives::{map::HashSet, Address};
10use alloy_provider::Provider;
11use clap::{Parser, ValueHint};
12use eyre::Result;
13use foundry_block_explorers::EtherscanApiVersion;
14use foundry_cli::{
15 opts::{EtherscanOpts, RpcOpts},
16 utils::{self, LoadConfig},
17};
18use foundry_common::{compile::ProjectCompiler, ContractsByArtifact};
19use foundry_compilers::{artifacts::EvmVersion, compilers::solc::Solc, info::ContractInfo};
20use foundry_config::{figment, impl_figment_convert, impl_figment_convert_cast, Config, SolcReq};
21use itertools::Itertools;
22use reqwest::Url;
23use semver::BuildMetadata;
24use std::path::PathBuf;
25
26#[derive(Clone, Debug, Parser)]
28pub struct VerifierArgs {
29 #[arg(long, help_heading = "Verifier options", default_value = "sourcify", value_enum)]
31 pub verifier: VerificationProviderType,
32
33 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_KEY")]
35 pub verifier_api_key: Option<String>,
36
37 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")]
39 pub verifier_url: Option<String>,
40
41 #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_VERSION")]
43 pub verifier_api_version: Option<EtherscanApiVersion>,
44}
45
46impl Default for VerifierArgs {
47 fn default() -> Self {
48 Self {
49 verifier: VerificationProviderType::Sourcify,
50 verifier_api_key: None,
51 verifier_url: None,
52 verifier_api_version: None,
53 }
54 }
55}
56
57#[derive(Clone, Debug, Parser)]
59pub struct VerifyArgs {
60 pub address: Address,
62
63 pub contract: Option<ContractInfo>,
65
66 #[arg(
68 long,
69 conflicts_with = "constructor_args_path",
70 value_name = "ARGS",
71 visible_alias = "encoded-constructor-args"
72 )]
73 pub constructor_args: Option<String>,
74
75 #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
77 pub constructor_args_path: Option<PathBuf>,
78
79 #[arg(long)]
81 pub guess_constructor_args: bool,
82
83 #[arg(long, value_name = "VERSION")]
85 pub compiler_version: Option<String>,
86
87 #[arg(long, value_name = "PROFILE_NAME")]
89 pub compilation_profile: Option<String>,
90
91 #[arg(long, visible_alias = "optimizer-runs", value_name = "NUM")]
93 pub num_of_optimizations: Option<usize>,
94
95 #[arg(long)]
97 pub flatten: bool,
98
99 #[arg(short, long)]
101 pub force: bool,
102
103 #[arg(long)]
105 pub skip_is_verified_check: bool,
106
107 #[arg(long)]
109 pub watch: bool,
110
111 #[arg(long, help_heading = "Linker options", env = "DAPP_LIBRARIES")]
113 pub libraries: Vec<String>,
114
115 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
120 pub root: Option<PathBuf>,
121
122 #[arg(long, conflicts_with = "flatten")]
127 pub show_standard_json_input: bool,
128
129 #[arg(long)]
131 pub via_ir: bool,
132
133 #[arg(long)]
137 pub evm_version: Option<EvmVersion>,
138
139 #[command(flatten)]
140 pub etherscan: EtherscanOpts,
141
142 #[command(flatten)]
143 pub rpc: RpcOpts,
144
145 #[command(flatten)]
146 pub retry: RetryArgs,
147
148 #[command(flatten)]
149 pub verifier: VerifierArgs,
150}
151
152impl_figment_convert!(VerifyArgs);
153
154impl figment::Provider for VerifyArgs {
155 fn metadata(&self) -> figment::Metadata {
156 figment::Metadata::named("Verify Provider")
157 }
158
159 fn data(
160 &self,
161 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
162 let mut dict = self.etherscan.dict();
163 dict.extend(self.rpc.dict());
164
165 if let Some(root) = self.root.as_ref() {
166 dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
167 }
168 if let Some(optimizer_runs) = self.num_of_optimizations {
169 dict.insert("optimizer".to_string(), figment::value::Value::serialize(true)?);
170 dict.insert(
171 "optimizer_runs".to_string(),
172 figment::value::Value::serialize(optimizer_runs)?,
173 );
174 }
175 if let Some(evm_version) = self.evm_version {
176 dict.insert("evm_version".to_string(), figment::value::Value::serialize(evm_version)?);
177 }
178 if self.via_ir {
179 dict.insert("via_ir".to_string(), figment::value::Value::serialize(self.via_ir)?);
180 }
181
182 if let Some(api_key) = &self.verifier.verifier_api_key {
183 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
184 }
185
186 if let Some(api_version) = &self.verifier.verifier_api_version {
187 dict.insert("etherscan_api_version".into(), api_version.to_string().into());
188 }
189
190 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
191 }
192}
193
194impl VerifyArgs {
195 pub async fn run(mut self) -> Result<()> {
197 let config = self.load_config()?;
198
199 if self.guess_constructor_args && config.get_rpc_url().is_none() {
200 eyre::bail!(
201 "You have to provide a valid RPC URL to use --guess-constructor-args feature"
202 )
203 }
204
205 let chain = match config.get_rpc_url() {
208 Some(_) => {
209 let provider = utils::get_provider(&config)?;
210 utils::get_chain(config.chain, provider).await?
211 }
212 None => config.chain.unwrap_or_default(),
213 };
214
215 let context = self.resolve_context().await?;
216
217 self.etherscan.chain = Some(chain);
219 self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
220
221 if self.show_standard_json_input {
222 let args = EtherscanVerificationProvider::default()
223 .create_verify_request(&self, &context)
224 .await?;
225 sh_println!("{}", args.source)?;
226 return Ok(())
227 }
228
229 let verifier_url = self.verifier.verifier_url.clone();
230 sh_println!("Start verifying contract `{}` deployed on {chain}", self.address)?;
231 if let Some(version) = &self.evm_version {
232 sh_println!("EVM version: {version}")?;
233 }
234 if let Some(version) = &self.compiler_version {
235 sh_println!("Compiler version: {version}")?;
236 }
237 if let Some(optimizations) = &self.num_of_optimizations {
238 sh_println!("Optimizations: {optimizations}")?
239 }
240 if let Some(args) = &self.constructor_args {
241 if !args.is_empty() {
242 sh_println!("Constructor args: {args}")?
243 }
244 }
245 self.verifier.verifier.client(self.etherscan.key().as_deref())?.verify(self, context).await.map_err(|err| {
246 if let Some(verifier_url) = verifier_url {
247 match Url::parse(&verifier_url) {
248 Ok(url) => {
249 if is_host_only(&url) {
250 return err.wrap_err(format!(
251 "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
252 ))
253 }
254 }
255 Err(url_err) => {
256 return err.wrap_err(format!(
257 "Invalid URL {verifier_url} provided: {url_err}"
258 ))
259 }
260 }
261 }
262
263 err
264 })
265 }
266
267 pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
269 self.verifier.verifier.client(self.etherscan.key().as_deref())
270 }
271
272 pub async fn resolve_context(&self) -> Result<VerificationContext> {
275 let mut config = self.load_config()?;
276 config.libraries.extend(self.libraries.clone());
277
278 let project = config.project()?;
279
280 if let Some(ref contract) = self.contract {
281 let contract_path = if let Some(ref path) = contract.path {
282 project.root().join(PathBuf::from(path))
283 } else {
284 project.find_contract_path(&contract.name)?
285 };
286
287 let cache = project.read_cache_file().ok();
288
289 let mut version = if let Some(ref version) = self.compiler_version {
290 version.trim_start_matches('v').parse()?
291 } else if let Some(ref solc) = config.solc {
292 match solc {
293 SolcReq::Version(version) => version.to_owned(),
294 SolcReq::Local(solc) => Solc::new(solc)?.version,
295 }
296 } else if let Some(entry) =
297 cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
298 {
299 let unique_versions = entry
300 .artifacts
301 .get(&contract.name)
302 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
303 .unwrap_or_default();
304
305 if unique_versions.is_empty() {
306 eyre::bail!("No matching artifact found for {}", contract.name);
307 } else if unique_versions.len() > 1 {
308 warn!(
309 "Ambiguous compiler versions found in cache: {}",
310 unique_versions.iter().join(", ")
311 );
312 eyre::bail!("Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag.")
313 }
314
315 unique_versions.into_iter().next().unwrap().to_owned()
316 } else {
317 eyre::bail!("If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml")
318 };
319
320 let settings = if let Some(profile) = &self.compilation_profile {
321 if profile == "default" {
322 &project.settings
323 } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
324 settings
325 } else {
326 eyre::bail!("Unknown compilation profile: {}", profile)
327 }
328 } else if let Some((cache, entry)) = cache
329 .as_ref()
330 .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
331 {
332 let profiles = entry
333 .artifacts
334 .get(&contract.name)
335 .and_then(|artifacts| {
336 let mut cached_artifacts = artifacts.get(&version);
337 if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
345 version.build = BuildMetadata::EMPTY;
346 cached_artifacts = artifacts.get(&version);
347 }
348 cached_artifacts
349 })
350 .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
351 .unwrap_or_default();
352
353 if profiles.is_empty() {
354 eyre::bail!("No matching artifact found for {}", contract.name);
355 } else if profiles.len() > 1 {
356 eyre::bail!("Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag", profiles.iter().join(", "))
357 }
358
359 let profile = profiles.into_iter().next().unwrap().to_owned();
360 let settings = cache.profiles.get(&profile).expect("must be present");
361
362 settings
363 } else if project.additional_settings.is_empty() {
364 &project.settings
365 } else {
366 eyre::bail!("If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml")
367 };
368
369 VerificationContext::new(
370 contract_path,
371 contract.name.clone(),
372 version,
373 config,
374 settings.clone(),
375 )
376 } else {
377 if config.get_rpc_url().is_none() {
378 eyre::bail!("You have to provide a contract name or a valid RPC URL")
379 }
380 let provider = utils::get_provider(&config)?;
381 let code = provider.get_code_at(self.address).await?;
382
383 let output = ProjectCompiler::new().compile(&project)?;
384 let contracts = ContractsByArtifact::new(
385 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
386 );
387
388 let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
389 eyre::bail!(format!(
390 "Bytecode at {} does not match any local contracts",
391 self.address
392 ))
393 };
394
395 let settings = project
396 .settings_profiles()
397 .find_map(|(name, settings)| {
398 (name == artifact_id.profile.as_str()).then_some(settings)
399 })
400 .expect("must be present");
401
402 VerificationContext::new(
403 artifact_id.source.clone(),
404 artifact_id.name.split('.').next().unwrap().to_owned(),
405 artifact_id.version.clone(),
406 config,
407 settings.clone(),
408 )
409 }
410 }
411}
412
413#[derive(Clone, Debug, Parser)]
415pub struct VerifyCheckArgs {
416 pub id: String,
422
423 #[command(flatten)]
424 pub retry: RetryArgs,
425
426 #[command(flatten)]
427 pub etherscan: EtherscanOpts,
428
429 #[command(flatten)]
430 pub verifier: VerifierArgs,
431}
432
433impl_figment_convert_cast!(VerifyCheckArgs);
434
435impl VerifyCheckArgs {
436 pub async fn run(self) -> Result<()> {
438 sh_println!(
439 "Checking verification status on {}",
440 self.etherscan.chain.unwrap_or_default()
441 )?;
442 self.verifier.verifier.client(self.etherscan.key().as_deref())?.check(self).await
443 }
444}
445
446impl figment::Provider for VerifyCheckArgs {
447 fn metadata(&self) -> figment::Metadata {
448 figment::Metadata::named("Verify Check Provider")
449 }
450
451 fn data(
452 &self,
453 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
454 let mut dict = self.etherscan.dict();
455 if let Some(api_key) = &self.etherscan.key {
456 dict.insert("etherscan_api_key".into(), api_key.as_str().into());
457 }
458
459 if let Some(api_version) = &self.etherscan.api_version {
460 dict.insert("etherscan_api_version".into(), api_version.to_string().into());
461 }
462
463 Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn can_parse_verify_contract() {
473 let args: VerifyArgs = VerifyArgs::parse_from([
474 "foundry-cli",
475 "0x0000000000000000000000000000000000000000",
476 "src/Domains.sol:Domains",
477 "--via-ir",
478 ]);
479 assert!(args.via_ir);
480 }
481}