1use crate::{
2 provider::{VerificationContext, VerificationProvider, VerificationProviderType},
3 retry::RETRY_CHECK_ON_VERIFY,
4 verify::{VerifyArgs, VerifyCheckArgs},
5};
6use alloy_json_abi::Function;
7use alloy_primitives::hex;
8use alloy_provider::Provider;
9use alloy_rpc_types::TransactionTrait;
10use eyre::{eyre, Context, OptionExt, Result};
11use foundry_block_explorers::{
12 errors::EtherscanError,
13 utils::lookup_compiler_version,
14 verify::{CodeFormat, VerifyContract},
15 Client,
16};
17use foundry_cli::utils::{get_provider, read_constructor_args_file, LoadConfig};
18use foundry_common::{abi::encode_function_args, retry::RetryError};
19use foundry_compilers::{artifacts::BytecodeObject, Artifact};
20use foundry_config::{Chain, Config};
21use foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER;
22use regex::Regex;
23use semver::{BuildMetadata, Version};
24use std::{fmt::Debug, sync::LazyLock};
25
26mod flatten;
27
28mod standard_json;
29
30pub static RE_BUILD_COMMIT: LazyLock<Regex> =
31 LazyLock::new(|| Regex::new(r"(?P<commit>commit\.[0-9,a-f]{8})").unwrap());
32
33#[derive(Clone, Debug, Default)]
34#[non_exhaustive]
35pub struct EtherscanVerificationProvider;
36
37trait EtherscanSourceProvider: Send + Sync + Debug {
41 fn source(
42 &self,
43 args: &VerifyArgs,
44 context: &VerificationContext,
45 ) -> Result<(String, String, CodeFormat)>;
46}
47
48#[async_trait::async_trait]
49impl VerificationProvider for EtherscanVerificationProvider {
50 async fn preflight_verify_check(
51 &mut self,
52 args: VerifyArgs,
53 context: VerificationContext,
54 ) -> Result<()> {
55 let _ = self.prepare_verify_request(&args, &context).await?;
56 Ok(())
57 }
58
59 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
60 let (etherscan, verify_args) = self.prepare_verify_request(&args, &context).await?;
61
62 if !args.skip_is_verified_check &&
63 self.is_contract_verified(ðerscan, &verify_args).await?
64 {
65 sh_println!(
66 "\nContract [{}] {:?} is already verified. Skipping verification.",
67 verify_args.contract_name,
68 verify_args.address.to_checksum(None)
69 )?;
70
71 return Ok(())
72 }
73
74 trace!(?verify_args, "submitting verification request");
75
76 let resp = args
77 .retry
78 .into_retry()
79 .run_async(|| async {
80 sh_println!(
81 "\nSubmitting verification for [{}] {}.",
82 verify_args.contract_name,
83 verify_args.address
84 )?;
85 let resp = etherscan
86 .submit_contract_verification(&verify_args)
87 .await
88 .wrap_err_with(|| {
89 let args = serde_json::to_string(&verify_args).unwrap();
91 error!(?args, "Failed to submit verification");
92 format!("Failed to submit contract verification, payload:\n{args}")
93 })?;
94
95 trace!(?resp, "Received verification response");
96
97 if resp.status == "0" {
98 if resp.result == "Contract source code already verified"
99 || resp.result == "Smart-contract already verified."
101 {
102 return Ok(None)
103 }
104
105 if resp.result.starts_with("Unable to locate ContractCode at") ||
106 resp.result.starts_with("The address is not a smart contract")
107 {
108 warn!("{}", resp.result);
109 return Err(eyre!("Could not detect the deployment."))
110 }
111
112 warn!("Failed verify submission: {:?}", resp);
113 sh_err!(
114 "Encountered an error verifying this contract:\nResponse: `{}`\nDetails:
115 `{}`",
116 resp.message,
117 resp.result
118 )?;
119 std::process::exit(1);
120 }
121
122 Ok(Some(resp))
123 })
124 .await?;
125
126 if let Some(resp) = resp {
127 sh_println!(
128 "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}",
129 resp.message,
130 resp.result,
131 etherscan.address_url(args.address)
132 )?;
133
134 if args.watch {
135 let check_args = VerifyCheckArgs {
136 id: resp.result,
137 etherscan: args.etherscan,
138 retry: RETRY_CHECK_ON_VERIFY,
139 verifier: args.verifier,
140 };
141 return self.check(check_args).await
142 }
143 } else {
144 sh_println!("Contract source code already verified")?;
145 }
146
147 Ok(())
148 }
149
150 async fn check(&self, args: VerifyCheckArgs) -> Result<()> {
152 let config = args.load_config()?;
153 let etherscan = self.client(
154 args.etherscan.chain.unwrap_or_default(),
155 &args.verifier.verifier,
156 args.verifier.verifier_url.as_deref(),
157 args.etherscan.key().as_deref(),
158 &config,
159 )?;
160 args.retry
161 .into_retry()
162 .run_async_until_break(|| async {
163 let resp = etherscan
164 .check_contract_verification_status(args.id.clone())
165 .await
166 .wrap_err("Failed to request verification status")
167 .map_err(RetryError::Retry)?;
168
169 trace!(?resp, "Received verification response");
170
171 let _ = sh_println!(
172 "Contract verification status:\nResponse: `{}`\nDetails: `{}`",
173 resp.message,
174 resp.result
175 );
176
177 if resp.result == "Pending in queue" {
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!("Contract failed to verify.")))
192 }
193
194 if resp.result == "Pass - Verified" {
195 let _ = sh_println!("Contract successfully verified");
196 }
197
198 Ok(())
199 })
200 .await
201 .wrap_err("Checking verification result failed")
202 }
203}
204
205impl EtherscanVerificationProvider {
206 fn source_provider(&self, args: &VerifyArgs) -> Box<dyn EtherscanSourceProvider> {
208 if args.flatten {
209 Box::new(flatten::EtherscanFlattenedSource)
210 } else {
211 Box::new(standard_json::EtherscanStandardJsonSource)
212 }
213 }
214
215 async fn prepare_verify_request(
217 &mut self,
218 args: &VerifyArgs,
219 context: &VerificationContext,
220 ) -> Result<(Client, VerifyContract)> {
221 let config = args.load_config()?;
222 let etherscan = self.client(
223 args.etherscan.chain.unwrap_or_default(),
224 &args.verifier.verifier,
225 args.verifier.verifier_url.as_deref(),
226 args.etherscan.key().as_deref(),
227 &config,
228 )?;
229 let verify_args = self.create_verify_request(args, context).await?;
230
231 Ok((etherscan, verify_args))
232 }
233
234 async fn is_contract_verified(
236 &self,
237 etherscan: &Client,
238 verify_contract: &VerifyContract,
239 ) -> Result<bool> {
240 let check = etherscan.contract_abi(verify_contract.address).await;
241
242 if let Err(err) = check {
243 match err {
244 EtherscanError::ContractCodeNotVerified(_) => return Ok(false),
245 error => return Err(error.into()),
246 }
247 }
248
249 Ok(true)
250 }
251
252 pub(crate) fn client(
254 &self,
255 chain: Chain,
256 verifier_type: &VerificationProviderType,
257 verifier_url: Option<&str>,
258 etherscan_key: Option<&str>,
259 config: &Config,
260 ) -> Result<Client> {
261 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?;
262
263 let etherscan_api_url = verifier_url
264 .or_else(|| etherscan_config.as_ref().map(|c| c.api_url.as_str()))
265 .map(str::to_owned);
266
267 let api_url = etherscan_api_url.as_deref();
268 let base_url = etherscan_config
269 .as_ref()
270 .and_then(|c| c.browser_url.as_deref())
271 .or_else(|| chain.etherscan_urls().map(|(_, url)| url));
272
273 let etherscan_key =
274 etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.as_str()));
275
276 let mut builder = Client::builder();
277
278 builder = if let Some(api_url) = api_url {
279 let api_url = api_url.trim_end_matches('/');
281
282 let is_etherscan = verifier_type.is_etherscan() ||
285 (verifier_type.is_sourcify() && etherscan_key.is_some());
286 let base_url = if !is_etherscan {
287 api_url.strip_prefix("/api").unwrap_or(api_url)
289 } else {
290 base_url.unwrap_or(api_url)
291 };
292 builder.with_chain_id(chain).with_api_url(api_url)?.with_url(base_url)?
293 } else {
294 builder.chain(chain)?
295 };
296
297 builder
298 .with_api_key(etherscan_key.unwrap_or_default())
299 .build()
300 .wrap_err("Failed to create Etherscan client")
301 }
302
303 pub async fn create_verify_request(
308 &mut self,
309 args: &VerifyArgs,
310 context: &VerificationContext,
311 ) -> Result<VerifyContract> {
312 let (source, contract_name, code_format) =
313 self.source_provider(args).source(args, context)?;
314
315 let mut compiler_version = context.compiler_version.clone();
316 compiler_version.build = match RE_BUILD_COMMIT.captures(compiler_version.build.as_str()) {
317 Some(cap) => BuildMetadata::new(cap.name("commit").unwrap().as_str())?,
318 _ => BuildMetadata::EMPTY,
319 };
320
321 let compiler_version =
322 format!("v{}", ensure_solc_build_metadata(context.compiler_version.clone()).await?);
323 let constructor_args = self.constructor_args(args, context).await?;
324 let mut verify_args =
325 VerifyContract::new(args.address, contract_name, source, compiler_version)
326 .constructor_arguments(constructor_args)
327 .code_format(code_format);
328
329 if args.via_ir {
330 verify_args = verify_args.via_ir(true);
335 }
336
337 if code_format == CodeFormat::SingleFile {
338 verify_args = if let Some(optimizations) = args.num_of_optimizations {
339 verify_args.optimized().runs(optimizations as u32)
340 } else if context.config.optimizer == Some(true) {
341 verify_args
342 .optimized()
343 .runs(context.config.optimizer_runs.unwrap_or(200).try_into()?)
344 } else {
345 verify_args.not_optimized()
346 };
347 }
348
349 Ok(verify_args)
350 }
351
352 async fn constructor_args(
356 &mut self,
357 args: &VerifyArgs,
358 context: &VerificationContext,
359 ) -> Result<Option<String>> {
360 if let Some(ref constructor_args_path) = args.constructor_args_path {
361 let abi = context.get_target_abi()?;
362 let constructor = abi
363 .constructor()
364 .ok_or_else(|| eyre!("Can't retrieve constructor info from artifact ABI."))?;
365 let func = Function {
366 name: "constructor".to_string(),
367 inputs: constructor.inputs.clone(),
368 outputs: vec![],
369 state_mutability: alloy_json_abi::StateMutability::NonPayable,
370 };
371 let encoded_args = encode_function_args(
372 &func,
373 read_constructor_args_file(constructor_args_path.to_path_buf())?,
374 )?;
375 let encoded_args = hex::encode(encoded_args);
376 return Ok(Some(encoded_args[8..].into()))
377 }
378 if args.guess_constructor_args {
379 return Ok(Some(self.guess_constructor_args(args, context).await?))
380 }
381
382 Ok(args.constructor_args.clone())
383 }
384
385 async fn guess_constructor_args(
390 &mut self,
391 args: &VerifyArgs,
392 context: &VerificationContext,
393 ) -> Result<String> {
394 let provider = get_provider(&context.config)?;
395 let client = self.client(
396 args.etherscan.chain.unwrap_or_default(),
397 &args.verifier.verifier,
398 args.verifier.verifier_url.as_deref(),
399 args.etherscan.key.as_deref(),
400 &context.config,
401 )?;
402
403 let creation_data = client.contract_creation_data(args.address).await?;
404 let transaction = provider
405 .get_transaction_by_hash(creation_data.transaction_hash)
406 .await?
407 .ok_or_eyre("Transaction not found")?;
408 let receipt = provider
409 .get_transaction_receipt(creation_data.transaction_hash)
410 .await?
411 .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?;
412
413 let maybe_creation_code = if receipt.contract_address == Some(args.address) {
414 transaction.inner.inner.input()
415 } else if transaction.to() == Some(DEFAULT_CREATE2_DEPLOYER) {
416 &transaction.inner.inner.input()[32..]
417 } else {
418 eyre::bail!("Fetching of constructor arguments is not supported for contracts created by contracts")
419 };
420
421 let output = context.project.compile_file(&context.target_path)?;
422 let artifact = output
423 .find(&context.target_path, &context.target_name)
424 .ok_or_eyre("Contract artifact wasn't found locally")?;
425 let bytecode = artifact
426 .get_bytecode_object()
427 .ok_or_eyre("Contract artifact does not contain bytecode")?;
428
429 let bytecode = match bytecode.as_ref() {
430 BytecodeObject::Bytecode(bytes) => Ok(bytes),
431 BytecodeObject::Unlinked(_) => {
432 Err(eyre!("You have to provide correct libraries to use --guess-constructor-args"))
433 }
434 }?;
435
436 if maybe_creation_code.starts_with(bytecode) {
437 let constructor_args = &maybe_creation_code[bytecode.len()..];
438 let constructor_args = hex::encode(constructor_args);
439 sh_println!("Identified constructor arguments: {constructor_args}")?;
440 Ok(constructor_args)
441 } else {
442 eyre::bail!("Local bytecode doesn't match on-chain bytecode")
443 }
444 }
445}
446
447async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
458 if version.build != BuildMetadata::EMPTY {
459 Ok(version)
460 } else {
461 Ok(lookup_compiler_version(&version).await?)
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use clap::Parser;
469 use foundry_common::fs;
470 use foundry_test_utils::{forgetest_async, str};
471 use tempfile::tempdir;
472
473 #[test]
474 fn can_extract_etherscan_verify_config() {
475 let temp = tempdir().unwrap();
476 let root = temp.path();
477
478 let config = r#"
479 [profile.default]
480
481 [etherscan]
482 mumbai = { key = "dummykey", chain = 80001, url = "https://api-testnet.polygonscan.com/" }
483 "#;
484
485 let toml_file = root.join(Config::FILE_NAME);
486 fs::write(toml_file, config).unwrap();
487
488 let args: VerifyArgs = VerifyArgs::parse_from([
489 "foundry-cli",
490 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
491 "src/Counter.sol:Counter",
492 "--chain",
493 "mumbai",
494 "--root",
495 root.as_os_str().to_str().unwrap(),
496 ]);
497
498 let config = args.load_config().unwrap();
499
500 let etherscan = EtherscanVerificationProvider::default();
501 let client = etherscan
502 .client(
503 args.etherscan.chain.unwrap_or_default(),
504 &args.verifier.verifier,
505 args.verifier.verifier_url.as_deref(),
506 args.etherscan.key().as_deref(),
507 &config,
508 )
509 .unwrap();
510 assert_eq!(client.etherscan_api_url().as_str(), "https://api-testnet.polygonscan.com/");
511
512 assert!(format!("{client:?}").contains("dummykey"));
513
514 let args: VerifyArgs = VerifyArgs::parse_from([
515 "foundry-cli",
516 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
517 "src/Counter.sol:Counter",
518 "--chain",
519 "mumbai",
520 "--verifier-url",
521 "https://verifier-url.com/",
522 "--root",
523 root.as_os_str().to_str().unwrap(),
524 ]);
525
526 let config = args.load_config().unwrap();
527
528 let etherscan = EtherscanVerificationProvider::default();
529 let client = etherscan
530 .client(
531 args.etherscan.chain.unwrap_or_default(),
532 &args.verifier.verifier,
533 args.verifier.verifier_url.as_deref(),
534 args.etherscan.key().as_deref(),
535 &config,
536 )
537 .unwrap();
538 assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
539 assert!(format!("{client:?}").contains("dummykey"));
540 }
541
542 #[tokio::test(flavor = "multi_thread")]
543 async fn fails_on_disabled_cache_and_missing_info() {
544 let temp = tempdir().unwrap();
545 let root = temp.path();
546 let root_path = root.as_os_str().to_str().unwrap();
547
548 let config = r"
549 [profile.default]
550 cache = false
551 ";
552
553 let toml_file = root.join(Config::FILE_NAME);
554 fs::write(toml_file, config).unwrap();
555
556 let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064";
557 let contract_name = "Counter";
558 let src_dir = "src";
559 fs::create_dir_all(root.join(src_dir)).unwrap();
560 let contract_path = format!("{src_dir}/Counter.sol");
561 fs::write(root.join(&contract_path), "").unwrap();
562
563 let args = VerifyArgs::parse_from([
565 "foundry-cli",
566 address,
567 &format!("{contract_path}:{contract_name}"),
568 "--root",
569 root_path,
570 ]);
571 let result = args.resolve_context().await;
572 assert!(result.is_err());
573 assert_eq!(
574 result.unwrap_err().to_string(),
575 "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
576 );
577 }
578
579 forgetest_async!(respects_path_for_duplicate, |prj, cmd| {
580 prj.add_source("Counter1", "contract Counter {}").unwrap();
581 prj.add_source("Counter2", "contract Counter {}").unwrap();
582
583 cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#"
584[COMPILING_FILES] with [SOLC_VERSION]
585...
586[SOLC_VERSION] [ELAPSED]
587Compiler run successful!
588
589"#]]);
590
591 let args = VerifyArgs::parse_from([
592 "foundry-cli",
593 "0x0000000000000000000000000000000000000000",
594 "src/Counter1.sol:Counter",
595 "--root",
596 &prj.root().to_string_lossy(),
597 ]);
598 let context = args.resolve_context().await.unwrap();
599
600 let mut etherscan = EtherscanVerificationProvider::default();
601 etherscan.preflight_verify_check(args, context).await.unwrap();
602 });
603}