Skip to main content

forge_verify/
verify.rs

1//! The `forge verify-bytecode` command.
2
3use crate::{
4    RetryArgs,
5    etherscan::EtherscanVerificationProvider,
6    provider::{VerificationContext, VerificationProvider, VerificationProviderType},
7    utils::is_host_only,
8};
9use alloy_primitives::{Address, TxHash, map::HashSet};
10use alloy_provider::Provider;
11use clap::{Parser, ValueEnum, ValueHint};
12use eyre::Result;
13use foundry_cli::{
14    opts::{EtherscanOpts, RpcOpts},
15    utils::{self, LoadConfig},
16};
17use foundry_common::{ContractsByArtifact, compile::ProjectCompiler};
18use foundry_compilers::{artifacts::EvmVersion, compilers::solc::Solc, info::ContractInfo};
19use foundry_config::{
20    Chain, Config, SolcReq, figment, impl_figment_convert, impl_figment_convert_cast,
21};
22use itertools::Itertools;
23use reqwest::Url;
24use semver::BuildMetadata;
25use std::path::PathBuf;
26
27/// The programming language used for smart contract development.
28///
29/// This enum represents the supported contract languages for verification.
30#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
31pub enum ContractLanguage {
32    /// Solidity programming language
33    Solidity,
34    /// Vyper programming language  
35    Vyper,
36}
37
38/// Verification provider arguments
39#[derive(Clone, Debug, Parser)]
40pub struct VerifierArgs {
41    /// The contract verification provider to use.
42    #[arg(long, help_heading = "Verifier options", default_value = "sourcify", value_enum)]
43    pub verifier: VerificationProviderType,
44
45    /// The verifier API KEY, if using a custom provider.
46    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_KEY")]
47    pub verifier_api_key: Option<String>,
48
49    /// The verifier URL, if using a custom provider.
50    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")]
51    pub verifier_url: Option<String>,
52}
53
54impl Default for VerifierArgs {
55    fn default() -> Self {
56        Self {
57            verifier: VerificationProviderType::Sourcify,
58            verifier_api_key: None,
59            verifier_url: None,
60        }
61    }
62}
63
64/// CLI arguments for `forge verify-contract`.
65#[derive(Clone, Debug, Parser)]
66pub struct VerifyArgs {
67    /// The address of the contract to verify.
68    pub address: Address,
69
70    /// The contract identifier in the form `<path>:<contractname>`.
71    pub contract: Option<ContractInfo>,
72
73    /// The ABI-encoded constructor arguments. Only for Etherscan.
74    #[arg(
75        long,
76        conflicts_with = "constructor_args_path",
77        value_name = "ARGS",
78        visible_alias = "encoded-constructor-args"
79    )]
80    pub constructor_args: Option<String>,
81
82    /// The path to a file containing the constructor arguments.
83    #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
84    pub constructor_args_path: Option<PathBuf>,
85
86    /// Try to extract constructor arguments from on-chain creation code.
87    #[arg(long)]
88    pub guess_constructor_args: bool,
89
90    /// The hash of the transaction which created the contract. Optional for Sourcify.
91    #[arg(long)]
92    pub creation_transaction_hash: Option<TxHash>,
93
94    /// The `solc` version to use to build the smart contract.
95    #[arg(long, value_name = "VERSION")]
96    pub compiler_version: Option<String>,
97
98    /// The compilation profile to use to build the smart contract.
99    #[arg(long, value_name = "PROFILE_NAME")]
100    pub compilation_profile: Option<String>,
101
102    /// The number of optimization runs used to build the smart contract.
103    #[arg(long, visible_alias = "optimizer-runs", value_name = "NUM")]
104    pub num_of_optimizations: Option<usize>,
105
106    /// Flatten the source code before verifying.
107    #[arg(long)]
108    pub flatten: bool,
109
110    /// Do not compile the flattened smart contract before verifying (if --flatten is passed).
111    #[arg(short, long)]
112    pub force: bool,
113
114    /// Do not check if the contract is already verified before verifying.
115    #[arg(long)]
116    pub skip_is_verified_check: bool,
117
118    /// Wait for verification result after submission.
119    #[arg(long)]
120    pub watch: bool,
121
122    /// Set pre-linked libraries.
123    #[arg(long, help_heading = "Linker options", env = "DAPP_LIBRARIES")]
124    pub libraries: Vec<String>,
125
126    /// The project's root path.
127    ///
128    /// By default root of the Git repository, if in one,
129    /// or the current working directory.
130    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
131    pub root: Option<PathBuf>,
132
133    /// Prints the standard json compiler input.
134    ///
135    /// The standard json compiler input can be used to manually submit contract verification in
136    /// the browser.
137    #[arg(long, conflicts_with = "flatten")]
138    pub show_standard_json_input: bool,
139
140    /// Use the Yul intermediate representation compilation pipeline.
141    #[arg(long)]
142    pub via_ir: bool,
143
144    /// The EVM version to use.
145    ///
146    /// Overrides the version specified in the config.
147    #[arg(long)]
148    pub evm_version: Option<EvmVersion>,
149
150    /// Do not auto-detect the `solc` version.
151    #[arg(long, help_heading = "Compiler options")]
152    pub no_auto_detect: bool,
153
154    /// Specify the solc version, or a path to a local solc, to build with.
155    ///
156    /// Valid values are in the format `x.y.z`, `solc:x.y.z` or `path/to/solc`.
157    #[arg(long = "use", help_heading = "Compiler options", value_name = "SOLC_VERSION")]
158    pub use_solc: Option<String>,
159
160    #[command(flatten)]
161    pub etherscan: EtherscanOpts,
162
163    #[command(flatten)]
164    pub rpc: RpcOpts,
165
166    #[command(flatten)]
167    pub retry: RetryArgs,
168
169    #[command(flatten)]
170    pub verifier: VerifierArgs,
171
172    /// The contract language (`solidity` or `vyper`).
173    ///
174    /// Defaults to `solidity` if none provided.
175    #[arg(long, value_enum)]
176    pub language: Option<ContractLanguage>,
177}
178
179impl_figment_convert!(VerifyArgs);
180
181impl figment::Provider for VerifyArgs {
182    fn metadata(&self) -> figment::Metadata {
183        figment::Metadata::named("Verify Provider")
184    }
185
186    fn data(
187        &self,
188    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
189        let mut dict = self.etherscan.dict();
190        dict.extend(self.rpc.dict());
191
192        if let Some(root) = self.root.as_ref() {
193            dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
194        }
195        if let Some(optimizer_runs) = self.num_of_optimizations {
196            dict.insert("optimizer".to_string(), figment::value::Value::serialize(true)?);
197            dict.insert(
198                "optimizer_runs".to_string(),
199                figment::value::Value::serialize(optimizer_runs)?,
200            );
201        }
202        if let Some(evm_version) = self.evm_version {
203            dict.insert("evm_version".to_string(), figment::value::Value::serialize(evm_version)?);
204        }
205        if self.via_ir {
206            dict.insert("via_ir".to_string(), figment::value::Value::serialize(self.via_ir)?);
207        }
208
209        if self.no_auto_detect {
210            dict.insert("auto_detect_solc".to_string(), figment::value::Value::serialize(false)?);
211        }
212
213        if let Some(ref solc) = self.use_solc {
214            let solc = solc.trim_start_matches("solc:");
215            dict.insert("solc".to_string(), figment::value::Value::serialize(solc)?);
216        }
217
218        if let Some(api_key) = &self.verifier.verifier_api_key {
219            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
220        }
221
222        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
223    }
224}
225
226impl VerifyArgs {
227    /// Run the verify command to submit the contract's source code for verification on etherscan
228    pub async fn run(mut self) -> Result<()> {
229        let config = self.load_config()?;
230
231        if self.guess_constructor_args && config.get_rpc_url().is_none() {
232            eyre::bail!(
233                "You have to provide a valid RPC URL to use --guess-constructor-args feature"
234            )
235        }
236
237        // If chain is not set, we try to get it from the RPC.
238        // If RPC is not set, the default chain is used.
239        let chain = match config.get_rpc_url() {
240            Some(_) => {
241                let provider = utils::get_provider(&config)?;
242                utils::get_chain(config.chain, provider).await?
243            }
244            None => config.chain.unwrap_or_default(),
245        };
246
247        let context = self.resolve_context().await?;
248
249        // Set Etherscan options.
250        self.etherscan.chain = Some(chain);
251        self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
252
253        // For chains with Sourcify-compatible APIs, use the chain's URL from etherscan_urls
254        if self.verifier.verifier.is_sourcify()
255            && self.verifier.verifier_url.is_none()
256            && let Some(url) = sourcify_api_url(chain)
257        {
258            self.verifier.verifier_url = Some(url);
259        }
260
261        if self.show_standard_json_input {
262            let args = EtherscanVerificationProvider::default()
263                .create_verify_request(&self, &context)
264                .await?;
265            sh_println!("{}", args.source)?;
266            return Ok(());
267        }
268
269        let verifier_url = self.verifier.verifier_url.clone();
270        sh_println!("Start verifying contract `{}` deployed on {chain}", self.address)?;
271        if let Some(version) = &self.evm_version {
272            sh_println!("EVM version: {version}")?;
273        }
274        if let Some(version) = &self.compiler_version {
275            sh_println!("Compiler version: {version}")?;
276        }
277        if let Some(optimizations) = &self.num_of_optimizations {
278            sh_println!("Optimizations:    {optimizations}")?
279        }
280        if let Some(args) = &self.constructor_args
281            && !args.is_empty()
282        {
283            sh_println!("Constructor args: {args}")?
284        }
285        self.verifier.verifier.client(self.etherscan.key().as_deref(), self.etherscan.chain, self.verifier.verifier_url.is_some())?.verify(self, context).await.map_err(|err| {
286            if let Some(verifier_url) = verifier_url {
287                 match Url::parse(&verifier_url) {
288                    Ok(url) => {
289                        if is_host_only(&url) {
290                            return err.wrap_err(format!(
291                                "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
292                            ))
293                        }
294                    }
295                    Err(url_err) => {
296                        return err.wrap_err(format!(
297                            "Invalid URL {verifier_url} provided: {url_err}"
298                        ))
299                    }
300                }
301            }
302
303            err
304        })
305    }
306
307    /// Returns the configured verification provider
308    pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
309        self.verifier.verifier.client(
310            self.etherscan.key().as_deref(),
311            self.etherscan.chain,
312            self.verifier.verifier_url.is_some(),
313        )
314    }
315
316    /// Resolves [VerificationContext] object either from entered contract name or by trying to
317    /// match bytecode located at given address.
318    pub async fn resolve_context(&self) -> Result<VerificationContext> {
319        let mut config = self.load_config()?;
320        config.libraries.extend(self.libraries.clone());
321
322        let project = config.project()?;
323
324        if let Some(ref contract) = self.contract {
325            let contract_path = if let Some(ref path) = contract.path {
326                project.root().join(PathBuf::from(path))
327            } else {
328                project.find_contract_path(&contract.name)?
329            };
330
331            let cache = project.read_cache_file().ok();
332
333            let mut version = if let Some(ref version) = self.compiler_version {
334                version.trim_start_matches('v').parse()?
335            } else if let Some(ref solc) = config.solc {
336                match solc {
337                    SolcReq::Version(version) => version.to_owned(),
338                    SolcReq::Local(solc) => Solc::new(solc)?.version,
339                }
340            } else if let Some(entry) =
341                cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
342            {
343                let unique_versions = entry
344                    .artifacts
345                    .get(&contract.name)
346                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
347                    .unwrap_or_default();
348
349                if unique_versions.is_empty() {
350                    eyre::bail!(
351                        "No matching artifact found for {}. This could be due to:\n\
352                        - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
353                        contract.name
354                    );
355                } else if unique_versions.len() > 1 {
356                    warn!(
357                        "Ambiguous compiler versions found in cache: {}",
358                        unique_versions.iter().join(", ")
359                    );
360                    eyre::bail!(
361                        "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
362                    )
363                }
364
365                unique_versions.into_iter().next().unwrap().to_owned()
366            } else {
367                eyre::bail!(
368                    "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
369                )
370            };
371
372            let settings = if let Some(profile) = &self.compilation_profile {
373                if profile == "default" {
374                    &project.settings
375                } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
376                    settings
377                } else {
378                    eyre::bail!("Unknown compilation profile: {}", profile)
379                }
380            } else if let Some((cache, entry)) = cache
381                .as_ref()
382                .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
383            {
384                let profiles = entry
385                    .artifacts
386                    .get(&contract.name)
387                    .and_then(|artifacts| {
388                        let mut cached_artifacts = artifacts.get(&version);
389                        // If we try to verify with specific build version and no cached artifacts
390                        // found, then check if we have artifacts cached for same version but
391                        // without any build metadata.
392                        // This could happen when artifacts are built / cached
393                        // with a version like `0.8.20` but verify is using a compiler-version arg
394                        // as `0.8.20+commit.a1b79de6`.
395                        // See <https://github.com/foundry-rs/foundry/issues/9510>.
396                        if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
397                            version.build = BuildMetadata::EMPTY;
398                            cached_artifacts = artifacts.get(&version);
399                        }
400                        cached_artifacts
401                    })
402                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
403                    .unwrap_or_default();
404
405                if profiles.is_empty() {
406                    eyre::bail!(
407                        "No matching artifact found for {} with compiler version {}. This could be due to:\n\
408                        - Compiler version mismatch - the contract was compiled with a different Solidity version",
409                        contract.name,
410                        version
411                    );
412                } else if profiles.len() > 1 {
413                    eyre::bail!(
414                        "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
415                        profiles.iter().join(", ")
416                    )
417                }
418
419                let profile = profiles.into_iter().next().unwrap().to_owned();
420                cache.profiles.get(&profile).expect("must be present")
421            } else if project.additional_settings.is_empty() {
422                &project.settings
423            } else {
424                eyre::bail!(
425                    "If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml"
426                )
427            };
428
429            VerificationContext::new(
430                contract_path,
431                contract.name.clone(),
432                version,
433                config,
434                settings.clone(),
435            )
436        } else {
437            if config.get_rpc_url().is_none() {
438                eyre::bail!("You have to provide a contract name or a valid RPC URL")
439            }
440            let provider = utils::get_provider(&config)?;
441            let code = provider.get_code_at(self.address).await?;
442
443            let output = ProjectCompiler::new().compile(&project)?;
444            let contracts = ContractsByArtifact::new(
445                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
446            );
447
448            let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
449                eyre::bail!(format!(
450                    "Bytecode at {} does not match any local contracts",
451                    self.address
452                ))
453            };
454
455            let settings = project
456                .settings_profiles()
457                .find_map(|(name, settings)| {
458                    (name == artifact_id.profile.as_str()).then_some(settings)
459                })
460                .expect("must be present");
461
462            VerificationContext::new(
463                artifact_id.source.clone(),
464                artifact_id.name.split('.').next().unwrap().to_owned(),
465                artifact_id.version.clone(),
466                config,
467                settings.clone(),
468            )
469        }
470    }
471
472    /// Detects the language for verification from source file extension, if none provided.
473    pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
474        self.language.unwrap_or_else(|| {
475            match ctx.target_path.extension().and_then(|e| e.to_str()) {
476                Some("vy") => ContractLanguage::Vyper,
477                _ => ContractLanguage::Solidity,
478            }
479        })
480    }
481}
482
483/// Check verification status arguments
484#[derive(Clone, Debug, Parser)]
485pub struct VerifyCheckArgs {
486    /// The verification ID.
487    ///
488    /// For Etherscan - Submission GUID.
489    ///
490    /// For Sourcify - Verification Job ID.
491    pub id: String,
492
493    #[command(flatten)]
494    pub retry: RetryArgs,
495
496    #[command(flatten)]
497    pub etherscan: EtherscanOpts,
498
499    #[command(flatten)]
500    pub verifier: VerifierArgs,
501}
502
503impl_figment_convert_cast!(VerifyCheckArgs);
504
505impl VerifyCheckArgs {
506    /// Run the verify command to submit the contract's source code for verification on etherscan
507    pub async fn run(self) -> Result<()> {
508        sh_println!(
509            "Checking verification status on {}",
510            self.etherscan.chain.unwrap_or_default()
511        )?;
512        self.verifier
513            .verifier
514            .client(
515                self.etherscan.key().as_deref(),
516                self.etherscan.chain,
517                self.verifier.verifier_url.is_some(),
518            )?
519            .check(self)
520            .await
521    }
522}
523
524impl figment::Provider for VerifyCheckArgs {
525    fn metadata(&self) -> figment::Metadata {
526        figment::Metadata::named("Verify Check Provider")
527    }
528
529    fn data(
530        &self,
531    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
532        let mut dict = self.etherscan.dict();
533        if let Some(api_key) = &self.etherscan.key {
534            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
535        }
536
537        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
538    }
539}
540
541/// Returns the Sourcify-compatible API URL for chains that have one registered in `etherscan_urls`.
542///
543/// Some chains register their Sourcify-compatible verification API under `etherscan_urls` in
544/// alloy-chains. This function returns the properly formatted URL for such chains.
545fn sourcify_api_url(chain: Chain) -> Option<String> {
546    if chain.is_custom_sourcify() {
547        chain.etherscan_urls().map(|(api_url, _)| {
548            let api_url = api_url.trim_end_matches('/');
549            format!("{api_url}/")
550        })
551    } else {
552        None
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn can_parse_verify_contract() {
562        let args: VerifyArgs = VerifyArgs::parse_from([
563            "foundry-cli",
564            "0x0000000000000000000000000000000000000000",
565            "src/Domains.sol:Domains",
566            "--via-ir",
567        ]);
568        assert!(args.via_ir);
569    }
570
571    #[test]
572    fn can_parse_new_compiler_flags() {
573        let args: VerifyArgs = VerifyArgs::parse_from([
574            "foundry-cli",
575            "0x0000000000000000000000000000000000000000",
576            "src/Domains.sol:Domains",
577            "--no-auto-detect",
578            "--use",
579            "0.8.23",
580        ]);
581        assert!(args.no_auto_detect);
582        assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
583    }
584}