forge_verify/
verify.rs

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