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, 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!(
324                        "No matching artifact found for {}. This could be due to:\n\
325                        - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
326                        contract.name
327                    );
328                } else if unique_versions.len() > 1 {
329                    warn!(
330                        "Ambiguous compiler versions found in cache: {}",
331                        unique_versions.iter().join(", ")
332                    );
333                    eyre::bail!(
334                        "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
335                    )
336                }
337
338                unique_versions.into_iter().next().unwrap().to_owned()
339            } else {
340                eyre::bail!(
341                    "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
342                )
343            };
344
345            let settings = if let Some(profile) = &self.compilation_profile {
346                if profile == "default" {
347                    &project.settings
348                } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
349                    settings
350                } else {
351                    eyre::bail!("Unknown compilation profile: {}", profile)
352                }
353            } else if let Some((cache, entry)) = cache
354                .as_ref()
355                .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
356            {
357                let profiles = entry
358                    .artifacts
359                    .get(&contract.name)
360                    .and_then(|artifacts| {
361                        let mut cached_artifacts = artifacts.get(&version);
362                        // If we try to verify with specific build version and no cached artifacts
363                        // found, then check if we have artifacts cached for same version but
364                        // without any build metadata.
365                        // This could happen when artifacts are built / cached
366                        // with a version like `0.8.20` but verify is using a compiler-version arg
367                        // as `0.8.20+commit.a1b79de6`.
368                        // See <https://github.com/foundry-rs/foundry/issues/9510>.
369                        if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
370                            version.build = BuildMetadata::EMPTY;
371                            cached_artifacts = artifacts.get(&version);
372                        }
373                        cached_artifacts
374                    })
375                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
376                    .unwrap_or_default();
377
378                if profiles.is_empty() {
379                    eyre::bail!(
380                        "No matching artifact found for {} with compiler version {}. This could be due to:\n\
381                        - Compiler version mismatch - the contract was compiled with a different Solidity version",
382                        contract.name,
383                        version
384                    );
385                } else if profiles.len() > 1 {
386                    eyre::bail!(
387                        "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
388                        profiles.iter().join(", ")
389                    )
390                }
391
392                let profile = profiles.into_iter().next().unwrap().to_owned();
393                cache.profiles.get(&profile).expect("must be present")
394            } else if project.additional_settings.is_empty() {
395                &project.settings
396            } else {
397                eyre::bail!(
398                    "If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml"
399                )
400            };
401
402            VerificationContext::new(
403                contract_path,
404                contract.name.clone(),
405                version,
406                config,
407                settings.clone(),
408            )
409        } else {
410            if config.get_rpc_url().is_none() {
411                eyre::bail!("You have to provide a contract name or a valid RPC URL")
412            }
413            let provider = utils::get_provider(&config)?;
414            let code = provider.get_code_at(self.address).await?;
415
416            let output = ProjectCompiler::new().compile(&project)?;
417            let contracts = ContractsByArtifact::new(
418                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
419            );
420
421            let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
422                eyre::bail!(format!(
423                    "Bytecode at {} does not match any local contracts",
424                    self.address
425                ))
426            };
427
428            let settings = project
429                .settings_profiles()
430                .find_map(|(name, settings)| {
431                    (name == artifact_id.profile.as_str()).then_some(settings)
432                })
433                .expect("must be present");
434
435            VerificationContext::new(
436                artifact_id.source.clone(),
437                artifact_id.name.split('.').next().unwrap().to_owned(),
438                artifact_id.version.clone(),
439                config,
440                settings.clone(),
441            )
442        }
443    }
444
445    /// Detects the language for verification from source file extension, if none provided.
446    pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
447        self.language.unwrap_or_else(|| {
448            match ctx.target_path.extension().and_then(|e| e.to_str()) {
449                Some("vy") => ContractLanguage::Vyper,
450                _ => ContractLanguage::Solidity,
451            }
452        })
453    }
454}
455
456/// Check verification status arguments
457#[derive(Clone, Debug, Parser)]
458pub struct VerifyCheckArgs {
459    /// The verification ID.
460    ///
461    /// For Etherscan - Submission GUID.
462    ///
463    /// For Sourcify - Contract Address.
464    pub id: String,
465
466    #[command(flatten)]
467    pub retry: RetryArgs,
468
469    #[command(flatten)]
470    pub etherscan: EtherscanOpts,
471
472    #[command(flatten)]
473    pub verifier: VerifierArgs,
474}
475
476impl_figment_convert_cast!(VerifyCheckArgs);
477
478impl VerifyCheckArgs {
479    /// Run the verify command to submit the contract's source code for verification on etherscan
480    pub async fn run(self) -> Result<()> {
481        sh_println!(
482            "Checking verification status on {}",
483            self.etherscan.chain.unwrap_or_default()
484        )?;
485        self.verifier.verifier.client(self.etherscan.key().as_deref())?.check(self).await
486    }
487}
488
489impl figment::Provider for VerifyCheckArgs {
490    fn metadata(&self) -> figment::Metadata {
491        figment::Metadata::named("Verify Check Provider")
492    }
493
494    fn data(
495        &self,
496    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
497        let mut dict = self.etherscan.dict();
498        if let Some(api_key) = &self.etherscan.key {
499            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
500        }
501
502        if let Some(api_version) = &self.etherscan.api_version {
503            dict.insert("etherscan_api_version".into(), api_version.to_string().into());
504        }
505
506        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn can_parse_verify_contract() {
516        let args: VerifyArgs = VerifyArgs::parse_from([
517            "foundry-cli",
518            "0x0000000000000000000000000000000000000000",
519            "src/Domains.sol:Domains",
520            "--via-ir",
521        ]);
522        assert!(args.via_ir);
523    }
524}