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