Skip to main content

forge_verify/etherscan/
mod.rs

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