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