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