forge_verify/etherscan/
mod.rs

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