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