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