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) if is_host_only(&url) => {
289                        return err.wrap_err(format!(
290                            "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
291                        ))
292                    }
293                    Err(url_err) => {
294                        return err.wrap_err(format!(
295                            "Invalid URL {verifier_url} provided: {url_err}"
296                        ))
297                    }
298                    _ => {}
299                }
300            }
301
302            err
303        })
304    }
305
306    /// Returns the configured verification provider
307    pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
308        self.verifier.verifier.client(
309            self.etherscan.key().as_deref(),
310            self.etherscan.chain,
311            self.verifier.verifier_url.is_some(),
312        )
313    }
314
315    /// Resolves [VerificationContext] object either from entered contract name or by trying to
316    /// match bytecode located at given address.
317    pub async fn resolve_context(&self) -> Result<VerificationContext> {
318        let mut config = self.load_config()?;
319        config.libraries.extend(self.libraries.clone());
320
321        let project = config.project()?;
322
323        if let Some(ref contract) = self.contract {
324            let contract_path = if let Some(ref path) = contract.path {
325                project.root().join(PathBuf::from(path))
326            } else {
327                project.find_contract_path(&contract.name)?
328            };
329
330            let cache = project.read_cache_file().ok();
331
332            let mut version = if let Some(ref version) = self.compiler_version {
333                version.trim_start_matches('v').parse()?
334            } else if let Some(ref solc) = config.solc {
335                match solc {
336                    SolcReq::Version(version) => version.to_owned(),
337                    SolcReq::Local(solc) => Solc::new(solc)?.version,
338                }
339            } else if let Some(entry) =
340                cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
341            {
342                let unique_versions = entry
343                    .artifacts
344                    .get(&contract.name)
345                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
346                    .unwrap_or_default();
347
348                if unique_versions.is_empty() {
349                    eyre::bail!(
350                        "No matching artifact found for {}. This could be due to:\n\
351                        - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
352                        contract.name
353                    );
354                } else if unique_versions.len() > 1 {
355                    warn!(
356                        "Ambiguous compiler versions found in cache: {}",
357                        unique_versions.iter().join(", ")
358                    );
359                    eyre::bail!(
360                        "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
361                    )
362                }
363
364                unique_versions.into_iter().next().unwrap().to_owned()
365            } else {
366                eyre::bail!(
367                    "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
368                )
369            };
370
371            let settings = if let Some(profile) = &self.compilation_profile {
372                if profile == "default" {
373                    &project.settings
374                } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
375                    settings
376                } else {
377                    eyre::bail!("Unknown compilation profile: {}", profile)
378                }
379            } else if let Some((cache, entry)) = cache
380                .as_ref()
381                .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
382            {
383                let profiles = entry
384                    .artifacts
385                    .get(&contract.name)
386                    .and_then(|artifacts| {
387                        let mut cached_artifacts = artifacts.get(&version);
388                        // If we try to verify with specific build version and no cached artifacts
389                        // found, then check if we have artifacts cached for same version but
390                        // without any build metadata.
391                        // This could happen when artifacts are built / cached
392                        // with a version like `0.8.20` but verify is using a compiler-version arg
393                        // as `0.8.20+commit.a1b79de6`.
394                        // See <https://github.com/foundry-rs/foundry/issues/9510>.
395                        if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
396                            version.build = BuildMetadata::EMPTY;
397                            cached_artifacts = artifacts.get(&version);
398                        }
399                        cached_artifacts
400                    })
401                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
402                    .unwrap_or_default();
403
404                if profiles.is_empty() {
405                    eyre::bail!(
406                        "No matching artifact found for {} with compiler version {}. This could be due to:\n\
407                        - Compiler version mismatch - the contract was compiled with a different Solidity version",
408                        contract.name,
409                        version
410                    );
411                } else if profiles.len() > 1 {
412                    eyre::bail!(
413                        "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
414                        profiles.iter().join(", ")
415                    )
416                }
417
418                let profile = profiles.into_iter().next().unwrap().to_owned();
419                cache.profiles.get(&profile).expect("must be present")
420            } else if project.additional_settings.is_empty() {
421                &project.settings
422            } else {
423                eyre::bail!(
424                    "If cache is disabled, compilation profile must be provided with `--compilation-profile` option or set in foundry.toml"
425                )
426            };
427
428            VerificationContext::new(
429                contract_path,
430                contract.name.clone(),
431                version,
432                config,
433                settings.clone(),
434            )
435        } else {
436            if config.get_rpc_url().is_none() {
437                eyre::bail!("You have to provide a contract name or a valid RPC URL")
438            }
439            let provider = utils::get_provider(&config)?;
440            let code = provider.get_code_at(self.address).await?;
441
442            let output = ProjectCompiler::new().compile(&project)?;
443            let contracts = ContractsByArtifact::new(
444                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
445            );
446
447            let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
448                eyre::bail!(format!(
449                    "Bytecode at {} does not match any local contracts",
450                    self.address
451                ))
452            };
453
454            let settings = project
455                .settings_profiles()
456                .find_map(|(name, settings)| {
457                    (name == artifact_id.profile.as_str()).then_some(settings)
458                })
459                .expect("must be present");
460
461            VerificationContext::new(
462                artifact_id.source.clone(),
463                artifact_id.name.split('.').next().unwrap().to_owned(),
464                artifact_id.version.clone(),
465                config,
466                settings.clone(),
467            )
468        }
469    }
470
471    /// Detects the language for verification from source file extension, if none provided.
472    pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
473        self.language.unwrap_or_else(|| {
474            match ctx.target_path.extension().and_then(|e| e.to_str()) {
475                Some("vy") => ContractLanguage::Vyper,
476                _ => ContractLanguage::Solidity,
477            }
478        })
479    }
480}
481
482/// Check verification status arguments
483#[derive(Clone, Debug, Parser)]
484pub struct VerifyCheckArgs {
485    /// The verification ID.
486    ///
487    /// For Etherscan - Submission GUID.
488    ///
489    /// For Sourcify - Verification Job ID.
490    pub id: String,
491
492    #[command(flatten)]
493    pub retry: RetryArgs,
494
495    #[command(flatten)]
496    pub etherscan: EtherscanOpts,
497
498    #[command(flatten)]
499    pub verifier: VerifierArgs,
500}
501
502impl_figment_convert_cast!(VerifyCheckArgs);
503
504impl VerifyCheckArgs {
505    /// Run the verify command to submit the contract's source code for verification on etherscan
506    pub async fn run(self) -> Result<()> {
507        sh_println!(
508            "Checking verification status on {}",
509            self.etherscan.chain.unwrap_or_default()
510        )?;
511        self.verifier
512            .verifier
513            .client(
514                self.etherscan.key().as_deref(),
515                self.etherscan.chain,
516                self.verifier.verifier_url.is_some(),
517            )?
518            .check(self)
519            .await
520    }
521}
522
523impl figment::Provider for VerifyCheckArgs {
524    fn metadata(&self) -> figment::Metadata {
525        figment::Metadata::named("Verify Check Provider")
526    }
527
528    fn data(
529        &self,
530    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
531        let mut dict = self.etherscan.dict();
532        if let Some(api_key) = &self.etherscan.key {
533            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
534        }
535
536        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
537    }
538}
539
540/// Returns the Sourcify-compatible API URL for chains that have one registered in `etherscan_urls`.
541///
542/// Some chains register their Sourcify-compatible verification API under `etherscan_urls` in
543/// alloy-chains. This function returns the properly formatted URL for such chains.
544fn sourcify_api_url(chain: Chain) -> Option<String> {
545    if chain.is_custom_sourcify() {
546        chain.etherscan_urls().map(|(api_url, _)| {
547            let api_url = api_url.trim_end_matches('/');
548            format!("{api_url}/")
549        })
550    } else {
551        None
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    #[test]
560    fn can_parse_verify_contract() {
561        let args: VerifyArgs = VerifyArgs::parse_from([
562            "foundry-cli",
563            "0x0000000000000000000000000000000000000000",
564            "src/Domains.sol:Domains",
565            "--via-ir",
566        ]);
567        assert!(args.via_ir);
568    }
569
570    #[test]
571    fn can_parse_new_compiler_flags() {
572        let args: VerifyArgs = VerifyArgs::parse_from([
573            "foundry-cli",
574            "0x0000000000000000000000000000000000000000",
575            "src/Domains.sol:Domains",
576            "--no-auto-detect",
577            "--use",
578            "0.8.23",
579        ]);
580        assert!(args.no_auto_detect);
581        assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
582    }
583}