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, map::HashSet};
10use alloy_provider::Provider;
11use clap::{Parser, ValueEnum, ValueHint};
12use eyre::Result;
13use foundry_block_explorers::EtherscanApiVersion;
14use foundry_cli::{
15    opts::{EtherscanOpts, RpcOpts},
16    utils::{self, LoadConfig},
17};
18use foundry_common::{ContractsByArtifact, compile::ProjectCompiler};
19use foundry_compilers::{artifacts::EvmVersion, compilers::solc::Solc, info::ContractInfo};
20use foundry_config::{Config, SolcReq, figment, impl_figment_convert, impl_figment_convert_cast};
21use itertools::Itertools;
22use reqwest::Url;
23use semver::BuildMetadata;
24use std::path::PathBuf;
25
26/// The programming language used for smart contract development.
27///
28/// This enum represents the supported contract languages for verification.
29#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
30pub enum ContractLanguage {
31    /// Solidity programming language
32    Solidity,
33    /// Vyper programming language  
34    Vyper,
35}
36
37/// Verification provider arguments
38#[derive(Clone, Debug, Parser)]
39pub struct VerifierArgs {
40    /// The contract verification provider to use.
41    #[arg(long, help_heading = "Verifier options", default_value = "sourcify", value_enum)]
42    pub verifier: VerificationProviderType,
43
44    /// The verifier API KEY, if using a custom provider.
45    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_KEY")]
46    pub verifier_api_key: Option<String>,
47
48    /// The verifier URL, if using a custom provider.
49    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")]
50    pub verifier_url: Option<String>,
51
52    /// The verifier API version, if using a custom provider.
53    #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_VERSION")]
54    pub verifier_api_version: Option<EtherscanApiVersion>,
55}
56
57impl Default for VerifierArgs {
58    fn default() -> Self {
59        Self {
60            verifier: VerificationProviderType::Sourcify,
61            verifier_api_key: None,
62            verifier_url: None,
63            verifier_api_version: None,
64        }
65    }
66}
67
68/// CLI arguments for `forge verify-contract`.
69#[derive(Clone, Debug, Parser)]
70pub struct VerifyArgs {
71    /// The address of the contract to verify.
72    pub address: Address,
73
74    /// The contract identifier in the form `<path>:<contractname>`.
75    pub contract: Option<ContractInfo>,
76
77    /// The ABI-encoded constructor arguments.
78    #[arg(
79        long,
80        conflicts_with = "constructor_args_path",
81        value_name = "ARGS",
82        visible_alias = "encoded-constructor-args"
83    )]
84    pub constructor_args: Option<String>,
85
86    /// The path to a file containing the constructor arguments.
87    #[arg(long, value_hint = ValueHint::FilePath, value_name = "PATH")]
88    pub constructor_args_path: Option<PathBuf>,
89
90    /// Try to extract constructor arguments from on-chain creation code.
91    #[arg(long)]
92    pub guess_constructor_args: bool,
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    #[command(flatten)]
151    pub etherscan: EtherscanOpts,
152
153    #[command(flatten)]
154    pub rpc: RpcOpts,
155
156    #[command(flatten)]
157    pub retry: RetryArgs,
158
159    #[command(flatten)]
160    pub verifier: VerifierArgs,
161
162    /// The contract language (`solidity` or `vyper`).
163    ///
164    /// Defaults to `solidity` if none provided.
165    #[arg(long, value_enum)]
166    pub language: Option<ContractLanguage>,
167}
168
169impl_figment_convert!(VerifyArgs);
170
171impl figment::Provider for VerifyArgs {
172    fn metadata(&self) -> figment::Metadata {
173        figment::Metadata::named("Verify Provider")
174    }
175
176    fn data(
177        &self,
178    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
179        let mut dict = self.etherscan.dict();
180        dict.extend(self.rpc.dict());
181
182        if let Some(root) = self.root.as_ref() {
183            dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
184        }
185        if let Some(optimizer_runs) = self.num_of_optimizations {
186            dict.insert("optimizer".to_string(), figment::value::Value::serialize(true)?);
187            dict.insert(
188                "optimizer_runs".to_string(),
189                figment::value::Value::serialize(optimizer_runs)?,
190            );
191        }
192        if let Some(evm_version) = self.evm_version {
193            dict.insert("evm_version".to_string(), figment::value::Value::serialize(evm_version)?);
194        }
195        if self.via_ir {
196            dict.insert("via_ir".to_string(), figment::value::Value::serialize(self.via_ir)?);
197        }
198
199        if let Some(api_key) = &self.verifier.verifier_api_key {
200            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
201        }
202
203        if let Some(api_version) = &self.verifier.verifier_api_version {
204            dict.insert("etherscan_api_version".into(), api_version.to_string().into());
205        }
206
207        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
208    }
209}
210
211impl VerifyArgs {
212    /// Run the verify command to submit the contract's source code for verification on etherscan
213    pub async fn run(mut self) -> Result<()> {
214        let config = self.load_config()?;
215
216        if self.guess_constructor_args && config.get_rpc_url().is_none() {
217            eyre::bail!(
218                "You have to provide a valid RPC URL to use --guess-constructor-args feature"
219            )
220        }
221
222        // If chain is not set, we try to get it from the RPC.
223        // If RPC is not set, the default chain is used.
224        let chain = match config.get_rpc_url() {
225            Some(_) => {
226                let provider = utils::get_provider(&config)?;
227                utils::get_chain(config.chain, provider).await?
228            }
229            None => config.chain.unwrap_or_default(),
230        };
231
232        let context = self.resolve_context().await?;
233
234        // Set Etherscan options.
235        self.etherscan.chain = Some(chain);
236        self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
237
238        if self.show_standard_json_input {
239            let args = EtherscanVerificationProvider::default()
240                .create_verify_request(&self, &context)
241                .await?;
242            sh_println!("{}", args.source)?;
243            return Ok(());
244        }
245
246        let verifier_url = self.verifier.verifier_url.clone();
247        sh_println!("Start verifying contract `{}` deployed on {chain}", self.address)?;
248        if let Some(version) = &self.evm_version {
249            sh_println!("EVM version: {version}")?;
250        }
251        if let Some(version) = &self.compiler_version {
252            sh_println!("Compiler version: {version}")?;
253        }
254        if let Some(optimizations) = &self.num_of_optimizations {
255            sh_println!("Optimizations:    {optimizations}")?
256        }
257        if let Some(args) = &self.constructor_args
258            && !args.is_empty()
259        {
260            sh_println!("Constructor args: {args}")?
261        }
262        self.verifier.verifier.client(self.etherscan.key().as_deref())?.verify(self, context).await.map_err(|err| {
263            if let Some(verifier_url) = verifier_url {
264                 match Url::parse(&verifier_url) {
265                    Ok(url) => {
266                        if is_host_only(&url) {
267                            return err.wrap_err(format!(
268                                "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
269                            ))
270                        }
271                    }
272                    Err(url_err) => {
273                        return err.wrap_err(format!(
274                            "Invalid URL {verifier_url} provided: {url_err}"
275                        ))
276                    }
277                }
278            }
279
280            err
281        })
282    }
283
284    /// Returns the configured verification provider
285    pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
286        self.verifier.verifier.client(self.etherscan.key().as_deref())
287    }
288
289    /// Resolves [VerificationContext] object either from entered contract name or by trying to
290    /// match bytecode located at given address.
291    pub async fn resolve_context(&self) -> Result<VerificationContext> {
292        let mut config = self.load_config()?;
293        config.libraries.extend(self.libraries.clone());
294
295        let project = config.project()?;
296
297        if let Some(ref contract) = self.contract {
298            let contract_path = if let Some(ref path) = contract.path {
299                project.root().join(PathBuf::from(path))
300            } else {
301                project.find_contract_path(&contract.name)?
302            };
303
304            let cache = project.read_cache_file().ok();
305
306            let mut version = if let Some(ref version) = self.compiler_version {
307                version.trim_start_matches('v').parse()?
308            } else if let Some(ref solc) = config.solc {
309                match solc {
310                    SolcReq::Version(version) => version.to_owned(),
311                    SolcReq::Local(solc) => Solc::new(solc)?.version,
312                }
313            } else if let Some(entry) =
314                cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
315            {
316                let unique_versions = entry
317                    .artifacts
318                    .get(&contract.name)
319                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
320                    .unwrap_or_default();
321
322                if unique_versions.is_empty() {
323                    eyre::bail!("No matching artifact found for {}", contract.name);
324                } else if unique_versions.len() > 1 {
325                    warn!(
326                        "Ambiguous compiler versions found in cache: {}",
327                        unique_versions.iter().join(", ")
328                    );
329                    eyre::bail!(
330                        "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
331                    )
332                }
333
334                unique_versions.into_iter().next().unwrap().to_owned()
335            } else {
336                eyre::bail!(
337                    "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
338                )
339            };
340
341            let settings = if let Some(profile) = &self.compilation_profile {
342                if profile == "default" {
343                    &project.settings
344                } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
345                    settings
346                } else {
347                    eyre::bail!("Unknown compilation profile: {}", profile)
348                }
349            } else if let Some((cache, entry)) = cache
350                .as_ref()
351                .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
352            {
353                let profiles = entry
354                    .artifacts
355                    .get(&contract.name)
356                    .and_then(|artifacts| {
357                        let mut cached_artifacts = artifacts.get(&version);
358                        // If we try to verify with specific build version and no cached artifacts
359                        // found, then check if we have artifacts cached for same version but
360                        // without any build metadata.
361                        // This could happen when artifacts are built / cached
362                        // with a version like `0.8.20` but verify is using a compiler-version arg
363                        // as `0.8.20+commit.a1b79de6`.
364                        // See <https://github.com/foundry-rs/foundry/issues/9510>.
365                        if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
366                            version.build = BuildMetadata::EMPTY;
367                            cached_artifacts = artifacts.get(&version);
368                        }
369                        cached_artifacts
370                    })
371                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
372                    .unwrap_or_default();
373
374                if profiles.is_empty() {
375                    eyre::bail!("No matching artifact found for {}", contract.name);
376                } else if profiles.len() > 1 {
377                    eyre::bail!(
378                        "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
379                        profiles.iter().join(", ")
380                    )
381                }
382
383                let profile = profiles.into_iter().next().unwrap().to_owned();
384                cache.profiles.get(&profile).expect("must be present")
385            } else if project.additional_settings.is_empty() {
386                &project.settings
387            } else {
388                eyre::bail!(
389                    "If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml"
390                )
391            };
392
393            VerificationContext::new(
394                contract_path,
395                contract.name.clone(),
396                version,
397                config,
398                settings.clone(),
399            )
400        } else {
401            if config.get_rpc_url().is_none() {
402                eyre::bail!("You have to provide a contract name or a valid RPC URL")
403            }
404            let provider = utils::get_provider(&config)?;
405            let code = provider.get_code_at(self.address).await?;
406
407            let output = ProjectCompiler::new().compile(&project)?;
408            let contracts = ContractsByArtifact::new(
409                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
410            );
411
412            let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
413                eyre::bail!(format!(
414                    "Bytecode at {} does not match any local contracts",
415                    self.address
416                ))
417            };
418
419            let settings = project
420                .settings_profiles()
421                .find_map(|(name, settings)| {
422                    (name == artifact_id.profile.as_str()).then_some(settings)
423                })
424                .expect("must be present");
425
426            VerificationContext::new(
427                artifact_id.source.clone(),
428                artifact_id.name.split('.').next().unwrap().to_owned(),
429                artifact_id.version.clone(),
430                config,
431                settings.clone(),
432            )
433        }
434    }
435
436    /// Detects the language for verification from source file extension, if none provided.
437    pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
438        self.language.unwrap_or_else(|| {
439            match ctx.target_path.extension().and_then(|e| e.to_str()) {
440                Some("vy") => ContractLanguage::Vyper,
441                _ => ContractLanguage::Solidity,
442            }
443        })
444    }
445}
446
447/// Check verification status arguments
448#[derive(Clone, Debug, Parser)]
449pub struct VerifyCheckArgs {
450    /// The verification ID.
451    ///
452    /// For Etherscan - Submission GUID.
453    ///
454    /// For Sourcify - Contract Address.
455    pub id: String,
456
457    #[command(flatten)]
458    pub retry: RetryArgs,
459
460    #[command(flatten)]
461    pub etherscan: EtherscanOpts,
462
463    #[command(flatten)]
464    pub verifier: VerifierArgs,
465}
466
467impl_figment_convert_cast!(VerifyCheckArgs);
468
469impl VerifyCheckArgs {
470    /// Run the verify command to submit the contract's source code for verification on etherscan
471    pub async fn run(self) -> Result<()> {
472        sh_println!(
473            "Checking verification status on {}",
474            self.etherscan.chain.unwrap_or_default()
475        )?;
476        self.verifier.verifier.client(self.etherscan.key().as_deref())?.check(self).await
477    }
478}
479
480impl figment::Provider for VerifyCheckArgs {
481    fn metadata(&self) -> figment::Metadata {
482        figment::Metadata::named("Verify Check Provider")
483    }
484
485    fn data(
486        &self,
487    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
488        let mut dict = self.etherscan.dict();
489        if let Some(api_key) = &self.etherscan.key {
490            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
491        }
492
493        if let Some(api_version) = &self.etherscan.api_version {
494            dict.insert("etherscan_api_version".into(), api_version.to_string().into());
495        }
496
497        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn can_parse_verify_contract() {
507        let args: VerifyArgs = VerifyArgs::parse_from([
508            "foundry-cli",
509            "0x0000000000000000000000000000000000000000",
510            "src/Domains.sol:Domains",
511            "--via-ir",
512        ]);
513        assert!(args.via_ir);
514    }
515}