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