forge_verify/etherscan/
mod.rs

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