forge_verify/
verify.rs

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