forge_verify/etherscan/
mod.rs

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