forge_verify/etherscan/
mod.rs

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