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