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