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