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_dyn_abi::JsonAbiExt;
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_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(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 values = encode_args(
441 &constructor.inputs,
442 read_constructor_args_file(constructor_args_path.clone())?,
443 )?;
444 return Ok(Some(hex::encode(constructor.abi_encode_input(&values)?)));
445 }
446 if args.guess_constructor_args {
447 return Ok(Some(self.guess_constructor_args(args, context).await?));
448 }
449
450 Ok(args.constructor_args.clone())
451 }
452
453 async fn guess_constructor_args(
458 &mut self,
459 args: &VerifyArgs,
460 context: &VerificationContext,
461 ) -> Result<String> {
462 let provider = get_provider(&context.config)?;
463 let client = self.client(&args.etherscan, &args.verifier, &context.config)?;
464
465 let creation_data = client.contract_creation_data(args.address).await?;
466 let transaction = provider
467 .get_transaction_by_hash(creation_data.transaction_hash)
468 .await?
469 .ok_or_eyre("Transaction not found")?;
470 let receipt = provider
471 .get_transaction_receipt(creation_data.transaction_hash)
472 .await?
473 .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?;
474
475 let maybe_creation_code = if receipt.contract_address == Some(args.address) {
476 transaction.input()
477 } else if transaction.to() == Some(DEFAULT_CREATE2_DEPLOYER) {
478 &transaction.input()[32..]
479 } else {
480 eyre::bail!(
481 "Fetching of constructor arguments is not supported for contracts created by contracts"
482 );
483 };
484
485 let output = context.project.compile_file(&context.target_path)?;
486 let artifact = output
487 .find(&context.target_path, &context.target_name)
488 .ok_or_eyre("Contract artifact wasn't found locally")?;
489 let bytecode = artifact
490 .get_bytecode_object()
491 .ok_or_eyre("Contract artifact does not contain bytecode")?;
492
493 let bytecode = match bytecode.as_ref() {
494 BytecodeObject::Bytecode(bytes) => Ok(bytes),
495 BytecodeObject::Unlinked(_) => {
496 Err(eyre!("You have to provide correct libraries to use --guess-constructor-args"))
497 }
498 }?;
499
500 if maybe_creation_code.starts_with(bytecode) {
501 let constructor_args = &maybe_creation_code[bytecode.len()..];
502 let constructor_args = hex::encode(constructor_args);
503 sh_status!("Identified constructor arguments: {constructor_args}")?;
504 Ok(constructor_args)
505 } else {
506 eyre::bail!("Local bytecode doesn't match on-chain bytecode");
507 }
508 }
509}
510
511fn sanitize_remote_message(message: &str) -> String {
512 message.chars().map(|ch| if ch.is_control() { ' ' } else { ch }).take(512).collect()
513}
514
515fn apply_license_type(verify_args: &mut VerifyContract, license_type: Option<&str>) {
516 if let Some(license_type) = license_type {
517 verify_args.other.insert("licenseType".to_string(), license_type.to_string());
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use clap::Parser;
525 use foundry_common::fs;
526 use foundry_test_utils::{forgetest, str, util::SOLC_VERSION};
527 use tempfile::tempdir;
528
529 #[test]
530 fn external_request_preserves_raw_input_and_target() {
531 let args =
532 VerifyArgs::parse_from(["foundry-cli", "0xd8509bee9c9bf012282ad33aba0d87241baf5064"]);
533 let input = serde_json::json!({
534 "language": "Solidity",
535 "settings": { "unknownSetting": { "futureField": true } },
536 "unknownTopLevel": [1, 2, 3]
537 });
538 let context = ExternalVerificationContext {
539 config: Config::default(),
540 compiler_version: "0.8.30+commit.73712a01".parse().unwrap(),
541 standard_json_input: std::sync::Arc::new(input.clone()),
542 target: "contracts/Unknown.sol:ExactTarget".to_string(),
543 };
544
545 let request =
546 EtherscanVerificationProvider.create_external_verify_request(&args, &context).unwrap();
547 assert_eq!(serde_json::from_str::<serde_json::Value>(&request.source).unwrap(), input);
548 assert_eq!(request.contract_name, "contracts/Unknown.sol:ExactTarget");
549 assert_eq!(request.compiler_version, "v0.8.30+commit.73712a01");
550 }
551
552 #[test]
553 fn applies_license_type_to_verify_request() {
554 let mut verify_args = VerifyContract::new(
555 Default::default(),
556 "Counter".to_string(),
557 "contract Counter {}".to_string(),
558 "v0.8.23+commit.f704f362".to_string(),
559 );
560
561 apply_license_type(&mut verify_args, Some("13"));
562
563 assert_eq!(verify_args.other.get("licenseType").map(String::as_str), Some("13"));
564 }
565
566 #[test]
567 fn can_extract_etherscan_verify_config() {
568 let temp = tempdir().unwrap();
569 let root = temp.path();
570
571 let config = r#"
572 [profile.default]
573
574 [etherscan]
575 amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
576 "#;
577
578 let toml_file = root.join(Config::FILE_NAME);
579 fs::write(toml_file, config).unwrap();
580
581 let args: VerifyArgs = VerifyArgs::parse_from([
582 "foundry-cli",
583 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
584 "src/Counter.sol:Counter",
585 "--chain",
586 "amoy",
587 "--root",
588 root.as_os_str().to_str().unwrap(),
589 ]);
590
591 let config = args.load_config().unwrap();
592
593 let etherscan = EtherscanVerificationProvider::default();
594 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
595 assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
597
598 assert!(format!("{client:?}").contains("dummykey"));
599
600 let args: VerifyArgs = VerifyArgs::parse_from([
601 "foundry-cli",
602 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
603 "src/Counter.sol:Counter",
604 "--chain",
605 "amoy",
606 "--verifier-url",
607 "https://verifier-url.com/",
608 "--root",
609 root.as_os_str().to_str().unwrap(),
610 ]);
611
612 let config = args.load_config().unwrap();
613
614 let etherscan = EtherscanVerificationProvider::default();
615 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
616 assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
617 assert!(format!("{client:?}").contains("dummykey"));
618 }
619
620 #[test]
621 fn can_extract_etherscan_v2_verify_config() {
622 let temp = tempdir().unwrap();
623 let root = temp.path();
624
625 let config = r#"
626 [profile.default]
627
628 [etherscan]
629 amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
630 "#;
631
632 let toml_file = root.join(Config::FILE_NAME);
633 fs::write(toml_file, config).unwrap();
634
635 let args: VerifyArgs = VerifyArgs::parse_from([
636 "foundry-cli",
637 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
638 "src/Counter.sol:Counter",
639 "--verifier",
640 "etherscan",
641 "--chain",
642 "amoy",
643 "--root",
644 root.as_os_str().to_str().unwrap(),
645 ]);
646
647 let config = args.load_config().unwrap();
648
649 let etherscan = EtherscanVerificationProvider::default();
650
651 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
652
653 assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
655 assert!(format!("{client:?}").contains("dummykey"));
656
657 let args: VerifyArgs = VerifyArgs::parse_from([
658 "foundry-cli",
659 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
660 "src/Counter.sol:Counter",
661 "--verifier",
662 "etherscan",
663 "--chain",
664 "amoy",
665 "--verifier-url",
666 "https://verifier-url.com/",
667 "--root",
668 root.as_os_str().to_str().unwrap(),
669 ]);
670
671 let config = args.load_config().unwrap();
672
673 assert_eq!(args.verifier.effective_type(), VerificationProviderType::Etherscan);
674
675 let etherscan = EtherscanVerificationProvider::default();
676 let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
677 assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
678 assert!(format!("{client:?}").contains("dummykey"));
679 }
680
681 #[tokio::test(flavor = "multi_thread")]
682 async fn fails_on_disabled_cache_and_missing_info() {
683 let temp = tempdir().unwrap();
684 let root = temp.path();
685 let root_path = root.as_os_str().to_str().unwrap();
686
687 let config = r"
688 [profile.default]
689 cache = false
690 ";
691
692 let toml_file = root.join(Config::FILE_NAME);
693 fs::write(toml_file, config).unwrap();
694
695 let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064";
696 let contract_name = "Counter";
697 let src_dir = "src";
698 fs::create_dir_all(root.join(src_dir)).unwrap();
699 let contract_path = format!("{src_dir}/Counter.sol");
700 fs::write(root.join(&contract_path), "").unwrap();
701
702 let args = VerifyArgs::parse_from([
704 "foundry-cli",
705 address,
706 &format!("{contract_path}:{contract_name}"),
707 "--root",
708 root_path,
709 ]);
710 let result = args.resolve_context().await;
711 assert!(result.is_err());
712 assert_eq!(
713 result.unwrap_err().to_string(),
714 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
715 );
716 }
717
718 #[forgetest]
719 async fn respects_path_for_duplicate(prj: _, cmd: _) {
720 prj.add_source("Counter1", "contract Counter {}");
721 prj.add_source("Counter2", "contract Counter {}");
722
723 cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#"
724[COMPILING_FILES] with [SOLC_VERSION]
725...
726[SOLC_VERSION] [ELAPSED]
727Compiler run successful!
728
729"#]]);
730
731 let args = VerifyArgs::parse_from([
732 "foundry-cli",
733 "0x0000000000000000000000000000000000000000",
734 "src/Counter1.sol:Counter",
735 "--root",
736 &prj.root().to_string_lossy(),
737 ]);
738 let context = args.resolve_context().await.unwrap();
739
740 let mut etherscan = EtherscanVerificationProvider::default();
741 etherscan.preflight_verify_check(args, context).await.unwrap();
742 }
743
744 #[forgetest]
745 async fn encodes_constructor_args_from_file(prj: _) {
746 let args_path = prj.root().join("constructor-args.json");
747 for (name, source, input, expected) in [
748 (
749 "Mixed",
750 "contract Mixed { constructor(uint256, string memory, uint256[] memory) {} }",
751 r#"["7", "hello", "[1,2]"]"#,
752 concat!(
753 "0000000000000000000000000000000000000000000000000000000000000007",
754 "0000000000000000000000000000000000000000000000000000000000000060",
755 "00000000000000000000000000000000000000000000000000000000000000a0",
756 "0000000000000000000000000000000000000000000000000000000000000005",
757 "68656c6c6f000000000000000000000000000000000000000000000000000000",
758 "0000000000000000000000000000000000000000000000000000000000000002",
759 "0000000000000000000000000000000000000000000000000000000000000001",
760 "0000000000000000000000000000000000000000000000000000000000000002",
761 ),
762 ),
763 ("Empty", "contract Empty { constructor() {} }", "[]", ""),
764 ] {
765 prj.add_source(name, source);
766 fs::write(&args_path, input).unwrap();
767 let args = VerifyArgs::parse_from([
768 "foundry-cli",
769 "0x0000000000000000000000000000000000000000",
770 &format!("src/{name}.sol:{name}"),
771 "--root",
772 &prj.root().to_string_lossy(),
773 "--constructor-args-path",
774 &args_path.to_string_lossy(),
775 "--compiler-version",
776 SOLC_VERSION,
777 ]);
778 let context = args.resolve_context().await.unwrap();
779 let mut etherscan = EtherscanVerificationProvider::default();
780 let encoded = etherscan.constructor_args(&args, &context).await.unwrap();
781 assert_eq!(encoded.as_deref(), Some(expected));
782
783 if name == "Mixed" {
784 fs::write(&args_path, r#"["7"]"#).unwrap();
785 let error = etherscan.constructor_args(&args, &context).await.unwrap_err();
786 assert_eq!(error.to_string(), "encode length mismatch: expected 3 types, got 1");
787 }
788 }
789 }
790}