1use crate::{
2 VerifierArgs,
3 provider::{VerificationContext, VerificationProvider, VerificationProviderType},
4 utils::ensure_solc_build_metadata,
5 verify::{ContractLanguage, VerifyArgs, VerifyCheckArgs},
6};
7use alloy_json_abi::Function;
8use alloy_primitives::hex;
9use alloy_provider::Provider;
10use alloy_rpc_types::TransactionTrait;
11use eyre::{Context, OptionExt, Result, eyre};
12use foundry_block_explorers::{
13 Client,
14 errors::EtherscanError,
15 verify::{CodeFormat, VerifyContract},
16};
17use foundry_cli::{
18 opts::EtherscanOpts,
19 utils::{LoadConfig, get_provider, read_constructor_args_file},
20};
21use foundry_common::{abi::encode_function_args, retry::RetryError};
22use foundry_compilers::{Artifact, artifacts::BytecodeObject};
23use foundry_config::Config;
24use foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER;
25use regex::Regex;
26use semver::BuildMetadata;
27use std::{fmt::Debug, sync::LazyLock};
28
29mod flatten;
30
31mod standard_json;
32
33pub static RE_BUILD_COMMIT: LazyLock<Regex> =
34 LazyLock::new(|| Regex::new(r"(?P<commit>commit\.[0-9,a-f]{8})").unwrap());
35
36#[derive(Clone, Debug, Default)]
37#[non_exhaustive]
38pub struct EtherscanVerificationProvider;
39
40trait EtherscanSourceProvider: Send + Sync + Debug {
44 fn source(
45 &self,
46 args: &VerifyArgs,
47 context: &VerificationContext,
48 ) -> Result<(String, String, CodeFormat)>;
49}
50
51#[async_trait::async_trait]
52impl VerificationProvider for EtherscanVerificationProvider {
53 fn provider_type(&self) -> VerificationProviderType {
54 VerificationProviderType::Etherscan
55 }
56
57 async fn preflight_verify_check(
58 &mut self,
59 args: VerifyArgs,
60 context: VerificationContext,
61 ) -> Result<()> {
62 let _ = self.prepare_verify_request(&args, &context).await?;
63 Ok(())
64 }
65
66 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
67 let (etherscan, verify_args) = self.prepare_verify_request(&args, &context).await?;
68
69 if !args.skip_is_verified_check
70 && self.is_contract_verified(ðerscan, &verify_args).await?
71 {
72 sh_status!(
73 "Contract [{}] {:?} is already verified. Skipping verification.",
74 verify_args.contract_name,
75 verify_args.address.to_checksum(None)
76 )?;
77
78 return Ok(());
79 }
80
81 trace!(?verify_args, "submitting verification request");
82
83 let resp = args
84 .retry
85 .into_retry()
86 .run_async(|| async {
87 sh_status!(
88 "Submitting verification for [{}] {}.",
89 verify_args.contract_name,
90 verify_args.address
91 )?;
92 let resp = etherscan
93 .submit_contract_verification(&verify_args)
94 .await
95 .wrap_err_with(|| {
96 let args = serde_json::to_string(&verify_args).unwrap();
98 format!("Failed to submit contract verification, payload:\n{args}")
99 })?;
100
101 trace!(?resp, "Received verification response");
102
103 if resp.status == "0" {
104 if resp.result == "Contract source code already verified"
105 || resp.result == "Smart-contract already verified."
107 {
108 return Ok(None);
109 }
110
111 if resp.result.starts_with("Unable to locate ContractCode at")
112 || resp.result.starts_with("The address is not a smart contract")
113 || resp.result.starts_with("Address is not a smart-contract")
114 {
115 warn!("{}", resp.result);
116 return Err(eyre!("Could not detect deployment: {}", resp.result));
117 }
118
119 sh_err!(
120 "Encountered an error verifying this contract:\nResponse: `{}`\nDetails:
121 `{}`",
122 resp.message,
123 resp.result
124 )?;
125 warn!("Failed verify submission: {:?}", resp);
126 std::process::exit(1);
127 }
128
129 Ok(Some(resp))
130 })
131 .await?;
132
133 if let Some(resp) = resp {
134 let url = etherscan.address_url(args.address);
135 sh_status!(
136 "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}",
137 resp.message,
138 resp.result,
139 url
140 )?;
141 sh_println!("{}\t{}", resp.result, url)?;
142
143 if args.watch {
144 let check_args = VerifyCheckArgs {
145 id: resp.result,
146 etherscan: args.etherscan,
147 retry: args.retry,
148 verifier: args.verifier,
149 };
150 return self.check(check_args).await;
151 }
152 } else {
153 sh_status!("Contract source code already verified")?;
154 }
155
156 Ok(())
157 }
158
159 async fn check(&self, args: VerifyCheckArgs) -> Result<()> {
161 let config = args.load_config()?;
162 let etherscan = self.client(&args.etherscan, &args.verifier, &config)?;
163 args.retry
164 .into_retry()
165 .run_async_until_break(|| async {
166 let resp = etherscan
167 .check_contract_verification_status(args.id.clone())
168 .await
169 .wrap_err("Failed to request verification status")
170 .map_err(RetryError::Retry)?;
171
172 trace!(?resp, "Received verification response");
173
174 let _ = sh_status!(
175 "Contract verification status:\nResponse: `{}`\nDetails: `{}`",
176 resp.message,
177 resp.result
178 );
179
180 if resp.result == "Pending in queue"
181 || resp.result.starts_with("Error: contract does not exist")
182 {
183 return Err(RetryError::Retry(eyre!("Verification is still pending...")));
184 }
185
186 if resp.result == "Unable to verify" {
187 return Err(RetryError::Retry(eyre!("Unable to verify.")));
188 }
189
190 if resp.result == "Already Verified" {
191 let _ = sh_status!("Contract source code already verified");
192 return Ok(());
193 }
194
195 if resp.status == "0" {
196 return Err(RetryError::Break(eyre!(
197 "Contract verification failed:\nStatus: `{}`\nResult: `{}`",
198 resp.status,
199 resp.result
200 )));
201 }
202
203 if resp.result == "Pass - Verified" {
204 let _ = sh_status!("Contract successfully verified");
205 }
206
207 Ok(())
208 })
209 .await
210 .wrap_err("Checking verification result failed")
211 }
212}
213
214impl EtherscanVerificationProvider {
215 fn source_provider(&self, args: &VerifyArgs) -> Box<dyn EtherscanSourceProvider> {
217 if args.flatten {
218 Box::new(flatten::EtherscanFlattenedSource)
219 } else {
220 Box::new(standard_json::EtherscanStandardJsonSource)
221 }
222 }
223
224 async fn prepare_verify_request(
226 &mut self,
227 args: &VerifyArgs,
228 context: &VerificationContext,
229 ) -> Result<(Client, VerifyContract)> {
230 let config = args.load_config()?;
231 let etherscan = self.client(&args.etherscan, &args.verifier, &config)?;
232 let verify_args = self.create_verify_request(args, context).await?;
233
234 Ok((etherscan, verify_args))
235 }
236
237 async fn is_contract_verified(
239 &self,
240 etherscan: &Client,
241 verify_contract: &VerifyContract,
242 ) -> Result<bool> {
243 let check = etherscan.contract_abi(verify_contract.address).await;
244
245 if let Err(err) = check {
246 return match err {
247 EtherscanError::ContractCodeNotVerified(_) => Ok(false),
248 error => Err(error).wrap_err_with(|| {
249 format!("Failed to obtain contract ABI for {}", verify_contract.address)
250 }),
251 };
252 }
253
254 Ok(true)
255 }
256
257 pub(crate) fn client(
259 &self,
260 etherscan_opts: &EtherscanOpts,
261 verifier_args: &VerifierArgs,
262 config: &Config,
263 ) -> Result<Client> {
264 let chain = etherscan_opts.chain.unwrap_or_default();
265 let etherscan_key = etherscan_opts.key();
266 let verifier_type = verifier_args.effective_type();
267 let verifier_url = verifier_args.verifier_url.as_deref();
268
269 let is_etherscan = verifier_type.is_etherscan()
272 || (verifier_type.is_sourcify() && etherscan_key.is_some());
273 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?;
274
275 let api_url =
276 verifier_url.or_else(|| etherscan_config.as_ref().map(|c| c.api_url.as_str()));
277 let base_url = etherscan_config
278 .as_ref()
279 .and_then(|c| c.browser_url.as_deref())
280 .or_else(|| chain.etherscan_urls().map(|(_, url)| url));
281 let etherscan_key =
282 etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.clone()));
283
284 let mut builder = Client::builder();
285
286 builder = if let Some(api_url) = api_url {
287 let api_url = api_url.trim_end_matches('/');
289 let base_url = if is_etherscan {
290 base_url.unwrap_or(api_url)
291 } else {
292 api_url.strip_suffix("/api").unwrap_or(api_url)
294 };
295 builder.with_api_url(api_url)?.with_url(base_url)?
296 } else {
297 builder.chain(chain)?
298 };
299
300 builder
301 .with_api_key(etherscan_key.unwrap_or_default())
302 .build()
303 .wrap_err("Failed to create Etherscan client")
304 }
305
306 pub async fn create_verify_request(
311 &mut self,
312 args: &VerifyArgs,
313 context: &VerificationContext,
314 ) -> Result<VerifyContract> {
315 let (source, contract_name, code_format) =
316 self.source_provider(args).source(args, context)?;
317
318 let lang = args.detect_language(context);
319
320 let mut compiler_version = context.compiler_version.clone();
321 compiler_version.build = match RE_BUILD_COMMIT.captures(compiler_version.build.as_str()) {
322 Some(cap) => BuildMetadata::new(cap.name("commit").unwrap().as_str())?,
323 _ => BuildMetadata::EMPTY,
324 };
325
326 let compiler_version = if matches!(lang, ContractLanguage::Vyper) {
327 format!("vyper:{}", compiler_version.to_string().split('+').next().unwrap_or("0.0.0"))
328 } else {
329 format!("v{}", ensure_solc_build_metadata(context.compiler_version.clone()).await?)
330 };
331
332 let constructor_args = self.constructor_args(args, context).await?;
333 let mut verify_args =
334 VerifyContract::new(args.address, contract_name, source, compiler_version)
335 .constructor_arguments(constructor_args)
336 .code_format(code_format);
337
338 if args.via_ir {
339 verify_args = verify_args.via_ir(true);
344 }
345
346 apply_license_type(&mut verify_args, args.license_type.as_deref());
347
348 if code_format == CodeFormat::SingleFile {
349 verify_args = if let Some(optimizations) = args.num_of_optimizations {
350 verify_args.optimized().runs(optimizations as u32)
351 } else if context.config.optimizer == Some(true) {
352 verify_args
353 .optimized()
354 .runs(context.config.optimizer_runs.unwrap_or(200).try_into()?)
355 } else {
356 verify_args.not_optimized()
357 };
358 }
359
360 if code_format == CodeFormat::VyperJson {
361 verify_args =
362 if args.num_of_optimizations.is_some() || context.config.optimizer == Some(true) {
363 verify_args.optimized().runs(1)
364 } else {
365 verify_args.not_optimized().runs(0)
366 }
367 }
368
369 Ok(verify_args)
370 }
371
372 async fn constructor_args(
376 &mut self,
377 args: &VerifyArgs,
378 context: &VerificationContext,
379 ) -> Result<Option<String>> {
380 if let Some(ref constructor_args_path) = args.constructor_args_path {
381 let abi = context.get_target_abi()?;
382 let constructor = abi
383 .constructor()
384 .ok_or_else(|| eyre!("Can't retrieve constructor info from artifact ABI."))?;
385 let func = Function {
386 name: "constructor".to_string(),
387 inputs: constructor.inputs.clone(),
388 outputs: vec![],
389 state_mutability: alloy_json_abi::StateMutability::NonPayable,
390 };
391 let encoded_args = encode_function_args(
392 &func,
393 read_constructor_args_file(constructor_args_path.clone())?,
394 )?;
395 let encoded_args = hex::encode(encoded_args);
396 return Ok(Some(encoded_args[8..].into()));
397 }
398 if args.guess_constructor_args {
399 return Ok(Some(self.guess_constructor_args(args, context).await?));
400 }
401
402 Ok(args.constructor_args.clone())
403 }
404
405 async fn guess_constructor_args(
410 &mut self,
411 args: &VerifyArgs,
412 context: &VerificationContext,
413 ) -> Result<String> {
414 let provider = get_provider(&context.config)?;
415 let client = self.client(&args.etherscan, &args.verifier, &context.config)?;
416
417 let creation_data = client.contract_creation_data(args.address).await?;
418 let transaction = provider
419 .get_transaction_by_hash(creation_data.transaction_hash)
420 .await?
421 .ok_or_eyre("Transaction not found")?;
422 let receipt = provider
423 .get_transaction_receipt(creation_data.transaction_hash)
424 .await?
425 .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?;
426
427 let maybe_creation_code = if receipt.contract_address == Some(args.address) {
428 transaction.input()
429 } else if transaction.to() == Some(DEFAULT_CREATE2_DEPLOYER) {
430 &transaction.input()[32..]
431 } else {
432 eyre::bail!(
433 "Fetching of constructor arguments is not supported for contracts created by contracts"
434 )
435 };
436
437 let output = context.project.compile_file(&context.target_path)?;
438 let artifact = output
439 .find(&context.target_path, &context.target_name)
440 .ok_or_eyre("Contract artifact wasn't found locally")?;
441 let bytecode = artifact
442 .get_bytecode_object()
443 .ok_or_eyre("Contract artifact does not contain bytecode")?;
444
445 let bytecode = match bytecode.as_ref() {
446 BytecodeObject::Bytecode(bytes) => Ok(bytes),
447 BytecodeObject::Unlinked(_) => {
448 Err(eyre!("You have to provide correct libraries to use --guess-constructor-args"))
449 }
450 }?;
451
452 if maybe_creation_code.starts_with(bytecode) {
453 let constructor_args = &maybe_creation_code[bytecode.len()..];
454 let constructor_args = hex::encode(constructor_args);
455 sh_status!("Identified constructor arguments: {constructor_args}")?;
456 Ok(constructor_args)
457 } else {
458 eyre::bail!("Local bytecode doesn't match on-chain bytecode")
459 }
460 }
461}
462
463fn apply_license_type(verify_args: &mut VerifyContract, license_type: Option<&str>) {
464 if let Some(license_type) = license_type {
465 verify_args.other.insert("licenseType".to_string(), license_type.to_string());
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use clap::Parser;
473 use foundry_common::fs;
474 use foundry_test_utils::{forgetest_async, str};
475 use tempfile::tempdir;
476
477 #[test]
478 fn applies_license_type_to_verify_request() {
479 let mut verify_args = VerifyContract::new(
480 Default::default(),
481 "Counter".to_string(),
482 "contract Counter {}".to_string(),
483 "v0.8.23+commit.f704f362".to_string(),
484 );
485
486 apply_license_type(&mut verify_args, Some("13"));
487
488 assert_eq!(verify_args.other.get("licenseType").map(String::as_str), Some("13"));
489 }
490
491 #[test]
492 fn can_extract_etherscan_verify_config() {
493 let temp = tempdir().unwrap();
494 let root = temp.path();
495
496 let config = r#"
497 [profile.default]
498
499 [etherscan]
500 amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
501 "#;
502
503 let toml_file = root.join(Config::FILE_NAME);
504 fs::write(toml_file, config).unwrap();
505
506 let args: VerifyArgs = VerifyArgs::parse_from([
507 "foundry-cli",
508 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
509 "src/Counter.sol:Counter",
510 "--chain",
511 "amoy",
512 "--root",
513 root.as_os_str().to_str().unwrap(),
514 ]);
515
516 let config = args.load_config().unwrap();
517
518 let etherscan = EtherscanVerificationProvider::default();
519 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
520 assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
522
523 assert!(format!("{client:?}").contains("dummykey"));
524
525 let args: VerifyArgs = VerifyArgs::parse_from([
526 "foundry-cli",
527 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
528 "src/Counter.sol:Counter",
529 "--chain",
530 "amoy",
531 "--verifier-url",
532 "https://verifier-url.com/",
533 "--root",
534 root.as_os_str().to_str().unwrap(),
535 ]);
536
537 let config = args.load_config().unwrap();
538
539 let etherscan = EtherscanVerificationProvider::default();
540 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
541 assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
542 assert!(format!("{client:?}").contains("dummykey"));
543 }
544
545 #[test]
546 fn can_extract_etherscan_v2_verify_config() {
547 let temp = tempdir().unwrap();
548 let root = temp.path();
549
550 let config = r#"
551 [profile.default]
552
553 [etherscan]
554 amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
555 "#;
556
557 let toml_file = root.join(Config::FILE_NAME);
558 fs::write(toml_file, config).unwrap();
559
560 let args: VerifyArgs = VerifyArgs::parse_from([
561 "foundry-cli",
562 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
563 "src/Counter.sol:Counter",
564 "--verifier",
565 "etherscan",
566 "--chain",
567 "amoy",
568 "--root",
569 root.as_os_str().to_str().unwrap(),
570 ]);
571
572 let config = args.load_config().unwrap();
573
574 let etherscan = EtherscanVerificationProvider::default();
575
576 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
577
578 assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
580 assert!(format!("{client:?}").contains("dummykey"));
581
582 let args: VerifyArgs = VerifyArgs::parse_from([
583 "foundry-cli",
584 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
585 "src/Counter.sol:Counter",
586 "--verifier",
587 "etherscan",
588 "--chain",
589 "amoy",
590 "--verifier-url",
591 "https://verifier-url.com/",
592 "--root",
593 root.as_os_str().to_str().unwrap(),
594 ]);
595
596 let config = args.load_config().unwrap();
597
598 assert_eq!(args.verifier.effective_type(), VerificationProviderType::Etherscan);
599
600 let etherscan = EtherscanVerificationProvider::default();
601 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
602 assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
603 assert!(format!("{client:?}").contains("dummykey"));
604 }
605
606 #[tokio::test(flavor = "multi_thread")]
607 async fn fails_on_disabled_cache_and_missing_info() {
608 let temp = tempdir().unwrap();
609 let root = temp.path();
610 let root_path = root.as_os_str().to_str().unwrap();
611
612 let config = r"
613 [profile.default]
614 cache = false
615 ";
616
617 let toml_file = root.join(Config::FILE_NAME);
618 fs::write(toml_file, config).unwrap();
619
620 let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064";
621 let contract_name = "Counter";
622 let src_dir = "src";
623 fs::create_dir_all(root.join(src_dir)).unwrap();
624 let contract_path = format!("{src_dir}/Counter.sol");
625 fs::write(root.join(&contract_path), "").unwrap();
626
627 let args = VerifyArgs::parse_from([
629 "foundry-cli",
630 address,
631 &format!("{contract_path}:{contract_name}"),
632 "--root",
633 root_path,
634 ]);
635 let result = args.resolve_context().await;
636 assert!(result.is_err());
637 assert_eq!(
638 result.unwrap_err().to_string(),
639 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
640 );
641 }
642
643 forgetest_async!(respects_path_for_duplicate, |prj, cmd| {
644 prj.add_source("Counter1", "contract Counter {}");
645 prj.add_source("Counter2", "contract Counter {}");
646
647 cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#"
648[COMPILING_FILES] with [SOLC_VERSION]
649...
650[SOLC_VERSION] [ELAPSED]
651Compiler run successful!
652
653"#]]);
654
655 let args = VerifyArgs::parse_from([
656 "foundry-cli",
657 "0x0000000000000000000000000000000000000000",
658 "src/Counter1.sol:Counter",
659 "--root",
660 &prj.root().to_string_lossy(),
661 ]);
662 let context = args.resolve_context().await.unwrap();
663
664 let mut etherscan = EtherscanVerificationProvider::default();
665 etherscan.preflight_verify_check(args, context).await.unwrap();
666 });
667}