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    /// Do not auto-detect the `solc` version.
149    #[arg(long, help_heading = "Compiler options")]
150    pub no_auto_detect: bool,
151
152    /// Specify the solc version, or a path to a local solc, to build with.
153    ///
154    /// Valid values are in the format `x.y.z`, `solc:x.y.z` or `path/to/solc`.
155    #[arg(long = "use", help_heading = "Compiler options", value_name = "SOLC_VERSION")]
156    pub use_solc: Option<String>,
157
158    #[command(flatten)]
159    pub etherscan: EtherscanOpts,
160
161    #[command(flatten)]
162    pub rpc: RpcOpts,
163
164    #[command(flatten)]
165    pub retry: RetryArgs,
166
167    #[command(flatten)]
168    pub verifier: VerifierArgs,
169
170    /// The contract language (`solidity` or `vyper`).
171    ///
172    /// Defaults to `solidity` if none provided.
173    #[arg(long, value_enum)]
174    pub language: Option<ContractLanguage>,
175}
176
177impl_figment_convert!(VerifyArgs);
178
179impl figment::Provider for VerifyArgs {
180    fn metadata(&self) -> figment::Metadata {
181        figment::Metadata::named("Verify Provider")
182    }
183
184    fn data(
185        &self,
186    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
187        let mut dict = self.etherscan.dict();
188        dict.extend(self.rpc.dict());
189
190        if let Some(root) = self.root.as_ref() {
191            dict.insert("root".to_string(), figment::value::Value::serialize(root)?);
192        }
193        if let Some(optimizer_runs) = self.num_of_optimizations {
194            dict.insert("optimizer".to_string(), figment::value::Value::serialize(true)?);
195            dict.insert(
196                "optimizer_runs".to_string(),
197                figment::value::Value::serialize(optimizer_runs)?,
198            );
199        }
200        if let Some(evm_version) = self.evm_version {
201            dict.insert("evm_version".to_string(), figment::value::Value::serialize(evm_version)?);
202        }
203        if self.via_ir {
204            dict.insert("via_ir".to_string(), figment::value::Value::serialize(self.via_ir)?);
205        }
206
207        if self.no_auto_detect {
208            dict.insert("auto_detect_solc".to_string(), figment::value::Value::serialize(false)?);
209        }
210
211        if let Some(ref solc) = self.use_solc {
212            let solc = solc.trim_start_matches("solc:");
213            dict.insert("solc".to_string(), figment::value::Value::serialize(solc)?);
214        }
215
216        if let Some(api_key) = &self.verifier.verifier_api_key {
217            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
218        }
219
220        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
221    }
222}
223
224impl VerifyArgs {
225    /// Run the verify command to submit the contract's source code for verification on etherscan
226    pub async fn run(mut self) -> Result<()> {
227        let config = self.load_config()?;
228
229        if self.guess_constructor_args && config.get_rpc_url().is_none() {
230            eyre::bail!(
231                "You have to provide a valid RPC URL to use --guess-constructor-args feature"
232            )
233        }
234
235        // If chain is not set, we try to get it from the RPC.
236        // If RPC is not set, the default chain is used.
237        let chain = match config.get_rpc_url() {
238            Some(_) => {
239                let provider = utils::get_provider(&config)?;
240                utils::get_chain(config.chain, provider).await?
241            }
242            None => config.chain.unwrap_or_default(),
243        };
244
245        let context = self.resolve_context().await?;
246
247        // Set Etherscan options.
248        self.etherscan.chain = Some(chain);
249        self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key);
250
251        if self.show_standard_json_input {
252            let args = EtherscanVerificationProvider::default()
253                .create_verify_request(&self, &context)
254                .await?;
255            sh_println!("{}", args.source)?;
256            return Ok(());
257        }
258
259        let verifier_url = self.verifier.verifier_url.clone();
260        sh_println!("Start verifying contract `{}` deployed on {chain}", self.address)?;
261        if let Some(version) = &self.evm_version {
262            sh_println!("EVM version: {version}")?;
263        }
264        if let Some(version) = &self.compiler_version {
265            sh_println!("Compiler version: {version}")?;
266        }
267        if let Some(optimizations) = &self.num_of_optimizations {
268            sh_println!("Optimizations:    {optimizations}")?
269        }
270        if let Some(args) = &self.constructor_args
271            && !args.is_empty()
272        {
273            sh_println!("Constructor args: {args}")?
274        }
275        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| {
276            if let Some(verifier_url) = verifier_url {
277                 match Url::parse(&verifier_url) {
278                    Ok(url) => {
279                        if is_host_only(&url) {
280                            return err.wrap_err(format!(
281                                "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?"
282                            ))
283                        }
284                    }
285                    Err(url_err) => {
286                        return err.wrap_err(format!(
287                            "Invalid URL {verifier_url} provided: {url_err}"
288                        ))
289                    }
290                }
291            }
292
293            err
294        })
295    }
296
297    /// Returns the configured verification provider
298    pub fn verification_provider(&self) -> Result<Box<dyn VerificationProvider>> {
299        self.verifier.verifier.client(
300            self.etherscan.key().as_deref(),
301            self.etherscan.chain,
302            self.verifier.verifier_url.is_some(),
303        )
304    }
305
306    /// Resolves [VerificationContext] object either from entered contract name or by trying to
307    /// match bytecode located at given address.
308    pub async fn resolve_context(&self) -> Result<VerificationContext> {
309        let mut config = self.load_config()?;
310        config.libraries.extend(self.libraries.clone());
311
312        let project = config.project()?;
313
314        if let Some(ref contract) = self.contract {
315            let contract_path = if let Some(ref path) = contract.path {
316                project.root().join(PathBuf::from(path))
317            } else {
318                project.find_contract_path(&contract.name)?
319            };
320
321            let cache = project.read_cache_file().ok();
322
323            let mut version = if let Some(ref version) = self.compiler_version {
324                version.trim_start_matches('v').parse()?
325            } else if let Some(ref solc) = config.solc {
326                match solc {
327                    SolcReq::Version(version) => version.to_owned(),
328                    SolcReq::Local(solc) => Solc::new(solc)?.version,
329                }
330            } else if let Some(entry) =
331                cache.as_ref().and_then(|cache| cache.files.get(&contract_path).cloned())
332            {
333                let unique_versions = entry
334                    .artifacts
335                    .get(&contract.name)
336                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
337                    .unwrap_or_default();
338
339                if unique_versions.is_empty() {
340                    eyre::bail!(
341                        "No matching artifact found for {}. This could be due to:\n\
342                        - Compiler version mismatch - the contract was compiled with a different Solidity version than what's being used for verification",
343                        contract.name
344                    );
345                } else if unique_versions.len() > 1 {
346                    warn!(
347                        "Ambiguous compiler versions found in cache: {}",
348                        unique_versions.iter().join(", ")
349                    );
350                    eyre::bail!(
351                        "Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag."
352                    )
353                }
354
355                unique_versions.into_iter().next().unwrap().to_owned()
356            } else {
357                eyre::bail!(
358                    "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
359                )
360            };
361
362            let settings = if let Some(profile) = &self.compilation_profile {
363                if profile == "default" {
364                    &project.settings
365                } else if let Some(settings) = project.additional_settings.get(profile.as_str()) {
366                    settings
367                } else {
368                    eyre::bail!("Unknown compilation profile: {}", profile)
369                }
370            } else if let Some((cache, entry)) = cache
371                .as_ref()
372                .and_then(|cache| Some((cache, cache.files.get(&contract_path)?.clone())))
373            {
374                let profiles = entry
375                    .artifacts
376                    .get(&contract.name)
377                    .and_then(|artifacts| {
378                        let mut cached_artifacts = artifacts.get(&version);
379                        // If we try to verify with specific build version and no cached artifacts
380                        // found, then check if we have artifacts cached for same version but
381                        // without any build metadata.
382                        // This could happen when artifacts are built / cached
383                        // with a version like `0.8.20` but verify is using a compiler-version arg
384                        // as `0.8.20+commit.a1b79de6`.
385                        // See <https://github.com/foundry-rs/foundry/issues/9510>.
386                        if cached_artifacts.is_none() && version.build != BuildMetadata::EMPTY {
387                            version.build = BuildMetadata::EMPTY;
388                            cached_artifacts = artifacts.get(&version);
389                        }
390                        cached_artifacts
391                    })
392                    .map(|artifacts| artifacts.keys().collect::<HashSet<_>>())
393                    .unwrap_or_default();
394
395                if profiles.is_empty() {
396                    eyre::bail!(
397                        "No matching artifact found for {} with compiler version {}. This could be due to:\n\
398                        - Compiler version mismatch - the contract was compiled with a different Solidity version",
399                        contract.name,
400                        version
401                    );
402                } else if profiles.len() > 1 {
403                    eyre::bail!(
404                        "Ambiguous compilation profiles found in cache: {}, please specify the profile through `--compilation-profile` flag",
405                        profiles.iter().join(", ")
406                    )
407                }
408
409                let profile = profiles.into_iter().next().unwrap().to_owned();
410                cache.profiles.get(&profile).expect("must be present")
411            } else if project.additional_settings.is_empty() {
412                &project.settings
413            } else {
414                eyre::bail!(
415                    "If cache is disabled, compilation profile must be provided with `--compiler-version` option or set in foundry.toml"
416                )
417            };
418
419            VerificationContext::new(
420                contract_path,
421                contract.name.clone(),
422                version,
423                config,
424                settings.clone(),
425            )
426        } else {
427            if config.get_rpc_url().is_none() {
428                eyre::bail!("You have to provide a contract name or a valid RPC URL")
429            }
430            let provider = utils::get_provider(&config)?;
431            let code = provider.get_code_at(self.address).await?;
432
433            let output = ProjectCompiler::new().compile(&project)?;
434            let contracts = ContractsByArtifact::new(
435                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
436            );
437
438            let Some((artifact_id, _)) = contracts.find_by_deployed_code_exact(&code) else {
439                eyre::bail!(format!(
440                    "Bytecode at {} does not match any local contracts",
441                    self.address
442                ))
443            };
444
445            let settings = project
446                .settings_profiles()
447                .find_map(|(name, settings)| {
448                    (name == artifact_id.profile.as_str()).then_some(settings)
449                })
450                .expect("must be present");
451
452            VerificationContext::new(
453                artifact_id.source.clone(),
454                artifact_id.name.split('.').next().unwrap().to_owned(),
455                artifact_id.version.clone(),
456                config,
457                settings.clone(),
458            )
459        }
460    }
461
462    /// Detects the language for verification from source file extension, if none provided.
463    pub fn detect_language(&self, ctx: &VerificationContext) -> ContractLanguage {
464        self.language.unwrap_or_else(|| {
465            match ctx.target_path.extension().and_then(|e| e.to_str()) {
466                Some("vy") => ContractLanguage::Vyper,
467                _ => ContractLanguage::Solidity,
468            }
469        })
470    }
471}
472
473/// Check verification status arguments
474#[derive(Clone, Debug, Parser)]
475pub struct VerifyCheckArgs {
476    /// The verification ID.
477    ///
478    /// For Etherscan - Submission GUID.
479    ///
480    /// For Sourcify - Verification Job ID.
481    pub id: String,
482
483    #[command(flatten)]
484    pub retry: RetryArgs,
485
486    #[command(flatten)]
487    pub etherscan: EtherscanOpts,
488
489    #[command(flatten)]
490    pub verifier: VerifierArgs,
491}
492
493impl_figment_convert_cast!(VerifyCheckArgs);
494
495impl VerifyCheckArgs {
496    /// Run the verify command to submit the contract's source code for verification on etherscan
497    pub async fn run(self) -> Result<()> {
498        sh_println!(
499            "Checking verification status on {}",
500            self.etherscan.chain.unwrap_or_default()
501        )?;
502        self.verifier
503            .verifier
504            .client(
505                self.etherscan.key().as_deref(),
506                self.etherscan.chain,
507                self.verifier.verifier_url.is_some(),
508            )?
509            .check(self)
510            .await
511    }
512}
513
514impl figment::Provider for VerifyCheckArgs {
515    fn metadata(&self) -> figment::Metadata {
516        figment::Metadata::named("Verify Check Provider")
517    }
518
519    fn data(
520        &self,
521    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
522        let mut dict = self.etherscan.dict();
523        if let Some(api_key) = &self.etherscan.key {
524            dict.insert("etherscan_api_key".into(), api_key.as_str().into());
525        }
526
527        Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn can_parse_verify_contract() {
537        let args: VerifyArgs = VerifyArgs::parse_from([
538            "foundry-cli",
539            "0x0000000000000000000000000000000000000000",
540            "src/Domains.sol:Domains",
541            "--via-ir",
542        ]);
543        assert!(args.via_ir);
544    }
545
546    #[test]
547    fn can_parse_new_compiler_flags() {
548        let args: VerifyArgs = VerifyArgs::parse_from([
549            "foundry-cli",
550            "0x0000000000000000000000000000000000000000",
551            "src/Domains.sol:Domains",
552            "--no-auto-detect",
553            "--use",
554            "0.8.23",
555        ]);
556        assert!(args.no_auto_detect);
557        assert_eq!(args.use_solc.as_deref(), Some("0.8.23"));
558    }
559}