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