diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 715941fa6..d9bb13544 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -354,9 +354,9 @@ impl CreateArgs { config.get_etherscan_config_with_chain(Some(chain.into()))?.map(|c| c.key); let context = if verify.zksync { - CompilerVerificationContext::Solc(verify.resolve_context().await?) - } else { CompilerVerificationContext::ZkSolc(verify.zk_resolve_context().await?) + } else { + CompilerVerificationContext::Solc(verify.resolve_context().await?) }; verify.verification_provider()?.preflight_check(verify, context).await?; diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index 07ce38444..1038566f8 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -36,6 +36,7 @@ use provider::VerificationProvider; pub mod bytecode; pub mod retry; mod sourcify; +mod zksync; pub use retry::RetryArgs; diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index b1e3aa088..660929ca3 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -1,6 +1,6 @@ use super::{ - etherscan::EtherscanVerificationProvider, sourcify::SourcifyVerificationProvider, VerifyArgs, - VerifyCheckArgs, + etherscan::EtherscanVerificationProvider, sourcify::SourcifyVerificationProvider, + zksync::ZkVerificationProvider, VerifyArgs, VerifyCheckArgs, }; use crate::zk_provider::CompilerVerificationContext; use alloy_json_abi::JsonAbi; @@ -128,6 +128,7 @@ impl FromStr for VerificationProviderType { "s" | "sourcify" => Ok(Self::Sourcify), "b" | "blockscout" => Ok(Self::Blockscout), "o" | "oklink" => Ok(Self::Oklink), + "z" | "zksync" => Ok(Self::ZKsync), _ => Err(format!("Unknown provider: {s}")), } } @@ -148,6 +149,9 @@ impl fmt::Display for VerificationProviderType { Self::Oklink => { write!(f, "oklink")?; } + Self::ZKsync => { + write!(f, "zksync")?; + } }; Ok(()) } @@ -160,6 +164,8 @@ pub enum VerificationProviderType { Sourcify, Blockscout, Oklink, + #[value(alias = "zksync")] + ZKsync, } impl VerificationProviderType { @@ -175,6 +181,7 @@ impl VerificationProviderType { Self::Sourcify => Ok(Box::::default()), Self::Blockscout => Ok(Box::::default()), Self::Oklink => Ok(Box::::default()), + Self::ZKsync => Ok(Box::::default()), } } } diff --git a/crates/verify/src/zk_provider.rs b/crates/verify/src/zk_provider.rs index 6fe158fca..38c30944f 100644 --- a/crates/verify/src/zk_provider.rs +++ b/crates/verify/src/zk_provider.rs @@ -1,8 +1,6 @@ use crate::provider::VerificationContext; -use super::{VerifyArgs, VerifyCheckArgs}; use alloy_json_abi::JsonAbi; -use async_trait::async_trait; use eyre::{OptionExt, Result}; use foundry_common::compile::ProjectCompiler; use foundry_compilers::{ @@ -124,29 +122,6 @@ impl ZkVerificationContext { } } -/// An abstraction for various verification providers such as etherscan, sourcify, blockscout -#[async_trait] -pub trait ZkVerificationProvider { - /// This should ensure the verify request can be prepared successfully. - /// - /// Caution: Implementers must ensure that this _never_ sends the actual verify request - /// `[VerificationProvider::verify]`, instead this is supposed to evaluate whether the given - /// [`VerifyArgs`] are valid to begin with. This should prevent situations where there's a - /// contract deployment that's executed before the verify request and the subsequent verify task - /// fails due to misconfiguration. - async fn preflight_check( - &mut self, - args: VerifyArgs, - context: ZkVerificationContext, - ) -> Result<()>; - - /// Sends the actual verify request for the targeted contract. - async fn verify(&mut self, args: VerifyArgs, context: ZkVerificationContext) -> Result<()>; - - /// Checks whether the contract is verified. - async fn check(&self, args: VerifyCheckArgs) -> Result<()>; -} - #[derive(Debug)] pub enum CompilerVerificationContext { Solc(VerificationContext), diff --git a/crates/verify/src/zksync/mod.rs b/crates/verify/src/zksync/mod.rs new file mode 100644 index 000000000..a53138505 --- /dev/null +++ b/crates/verify/src/zksync/mod.rs @@ -0,0 +1,354 @@ +use super::{provider::VerificationProvider, EtherscanOpts, VerifyArgs, VerifyCheckArgs}; +use crate::zk_provider::{CompilerVerificationContext, ZkVerificationContext}; +use alloy_json_abi::Function; +use alloy_primitives::hex; +use eyre::{eyre, Result}; +use foundry_common::{abi::encode_function_args, retry::Retry}; +use foundry_compilers::zksolc::input::StandardJsonCompilerInput; +use futures::FutureExt; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, thread::sleep, time::Duration}; + +pub mod standard_json; + +// TODO: This is hardcode for Sepolia verification URL, need to be updated. +pub static ZKSYNC_VERIFICATION_URL: &str = + "https://explorer.sepolia.era.zksync.dev/contract_verification"; + +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct ZkVerificationProvider; + +pub trait ZksyncSourceProvider: Send + Sync + Debug { + fn zk_source( + &self, + args: &VerifyArgs, + context: &ZkVerificationContext, + ) -> Result<(StandardJsonCompilerInput, String)>; +} + +#[async_trait::async_trait] +impl VerificationProvider for ZkVerificationProvider { + async fn preflight_check( + &mut self, + args: VerifyArgs, + context: CompilerVerificationContext, + ) -> Result<()> { + let _ = self.prepare_request(&args, &context).await?; + Ok(()) + } + + async fn verify( + &mut self, + args: VerifyArgs, + context: CompilerVerificationContext, + ) -> Result<()> { + trace!("ZkVerificationProvider::verify"); + let request = self.prepare_request(&args, &context).await?; + + let client = reqwest::Client::new(); + + let retry: Retry = args.retry.into(); + let verification_id: u64 = retry + .run_async(|| { + async { + println!( + "\nSubmitting verification for [{}] at address {}.", + request.contract_name, request.contract_address + ); + + let verifier_url = args + .verifier + .verifier_url + .as_deref() + .unwrap_or(ZKSYNC_VERIFICATION_URL); + + let response = client + .post(verifier_url) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await?; + + let status = response.status(); + let text = response.text().await?; + + if !status.is_success() { + eyre::bail!( + "Verification request for address ({}) failed with status code {}\nDetails: {}", + args.address, + status, + text, + ); + } + + let parsed_id = text.trim().parse().map_err(|e| { + eyre::eyre!("Failed to parse verification ID: {} - error: {}", text, e) + })?; + + Ok(parsed_id) + } + .boxed() + }) + .await?; + + println!("Verification submitted successfully. Verification ID: {}", verification_id); + + self.check(VerifyCheckArgs { + id: verification_id.to_string(), + verifier: args.verifier.clone(), + retry: args.retry, + etherscan: EtherscanOpts::default(), + }) + .await?; + + Ok(()) + } + + async fn check(&self, args: VerifyCheckArgs) -> Result<()> { + let max_retries = args.retry.retries; + let delay_in_seconds = args.retry.delay; + + let client = reqwest::Client::new(); + let base_url = args.verifier.verifier_url.as_deref().unwrap_or(ZKSYNC_VERIFICATION_URL); + let url = format!("{}/{}", base_url, args.id); + + let verification_status = + self.retry_verification_status(&client, &url, max_retries, delay_in_seconds).await?; + + self.process_status_response(Some(verification_status)) + } +} + +impl ZkVerificationProvider { + fn source_provider(&self) -> Box { + Box::new(standard_json::ZksyncStandardJsonSource) + } + async fn prepare_request( + &mut self, + args: &VerifyArgs, + context: &CompilerVerificationContext, + ) -> Result { + let (source, contract_name) = if let CompilerVerificationContext::ZkSolc(context) = context + { + self.source_provider().zk_source(args, context)? + } else { + eyre::bail!("Unsupported compiler context: only ZkSolc is supported"); + }; + + let (solc_version, zk_compiler_version) = match context { + CompilerVerificationContext::ZkSolc(zk_context) => { + // Format solc_version as "zkVM-{compiler_version}-1.0.1" + let solc_version = format!("zkVM-{}-1.0.1", zk_context.compiler_version.solc); + let zk_compiler_version = format!("v{}", zk_context.compiler_version.zksolc); + (solc_version, zk_compiler_version) + } + _ => { + return Err(eyre::eyre!( + "Expected context to be of type ZkSolc, but received a different type." + )); + } + }; + let optimization_used = source.settings.optimizer.enabled.unwrap_or(false); + // TODO: investigate runs better. Currently not included in the verification request. + let _runs = args.num_of_optimizations.map(|n| n as u64); + let constructor_args = self.constructor_args(args, context).await?.unwrap_or_default(); + + let request = VerifyContractRequest { + contract_address: args.address.to_string(), + source_code: source, + code_format: "solidity-standard-json-input".to_string(), + contract_name, + compiler_version: solc_version, + zk_compiler_version, + constructor_arguments: constructor_args, + optimization_used, + }; + + Ok(request) + } + + async fn constructor_args( + &mut self, + args: &VerifyArgs, + context: &CompilerVerificationContext, + ) -> Result> { + if let Some(ref constructor_args_path) = args.constructor_args_path { + let abi = context.get_target_abi()?; + let constructor = abi + .constructor() + .ok_or_else(|| eyre!("Can't retrieve constructor info from artifact ABI."))?; + #[allow(deprecated)] + let func = Function { + name: "constructor".to_string(), + inputs: constructor.inputs.clone(), + outputs: vec![], + state_mutability: alloy_json_abi::StateMutability::NonPayable, + }; + let encoded_args = encode_function_args( + &func, + foundry_cli::utils::read_constructor_args_file( + constructor_args_path.to_path_buf(), + )?, + )?; + let encoded_args = hex::encode(encoded_args); + return Ok(Some(format!("0x{}", &encoded_args[8..]))); + } + + if let Some(ref args) = args.constructor_args { + if args.starts_with("0x") { + return Ok(Some(args.clone())); + } else { + return Ok(Some(format!("0x{}", args))); + } + } + + Ok(Some("0x".to_string())) + } + /// Retry logic for checking the verification status + async fn retry_verification_status( + &self, + client: &reqwest::Client, + url: &str, + max_retries: u32, + delay_in_seconds: u32, + ) -> Result { + let mut retries = 0; + + loop { + let response = client.get(url).send().await?; + let status = response.status(); + let text = response.text().await?; + + if !status.is_success() { + eyre::bail!( + "Failed to request verification status with status code {}\nDetails: {}", + status, + text + ); + } + + let resp: ContractVerificationStatusResponse = serde_json::from_str(&text)?; + + if resp.error_exists() { + eyre::bail!("Verification error: {}", resp.get_error()); + } + if resp.is_verification_success() { + return Ok(resp); + } + if resp.is_verification_failure() { + eyre::bail!("Verification failed: {}", resp.get_error()); + } + if resp.is_pending() || resp.is_queued() { + if retries >= max_retries { + println!("Verification is still pending after {} retries.", max_retries); + return Ok(resp); + } + + retries += 1; + + // Calculate the next delay and wait + let delay_in_ms = calculate_retry_delay(retries, delay_in_seconds); + sleep(Duration::from_millis(delay_in_ms)); + } + } + } + fn process_status_response( + &self, + response: Option, + ) -> Result<()> { + if let Some(resp) = response { + match resp.status { + VerificationStatusEnum::Successful => { + println!("Verification was successful."); + } + VerificationStatusEnum::Failed => { + let error_message = resp.get_error(); + eyre::bail!("Verification failed: {}", error_message); + } + VerificationStatusEnum::Queued => { + println!("Verification is queued."); + } + VerificationStatusEnum::InProgress => { + println!("Verification is in progress."); + } + } + } else { + eyre::bail!("Empty response from verification status endpoint"); + } + Ok(()) + } +} + +/// Calculate the delay for the next retry attempt +fn calculate_retry_delay(retries: u32, base_delay_in_seconds: u32) -> u64 { + let base_delay_in_ms = (base_delay_in_seconds * 1000) as u64; + base_delay_in_ms * (1 << retries.min(5)) +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum VerificationStatusEnum { + Successful, + Failed, + Queued, + InProgress, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ContractVerificationStatusResponse { + pub status: VerificationStatusEnum, + pub error: Option, + pub compilation_errors: Option>, +} + +impl ContractVerificationStatusResponse { + pub fn error_exists(&self) -> bool { + self.error.is_some() || self.compilation_errors.is_some() + } + pub fn get_error(&self) -> String { + let mut errors = String::new(); + + if let Some(ref error) = self.error { + errors.push_str(error); + } + + if let Some(ref compilation_errors) = self.compilation_errors { + errors.push_str(&compilation_errors.join("\n")); + } + + errors + } + pub fn is_pending(&self) -> bool { + matches!(self.status, VerificationStatusEnum::InProgress) + } + pub fn is_verification_failure(&self) -> bool { + matches!(self.status, VerificationStatusEnum::Failed) + } + pub fn is_queued(&self) -> bool { + matches!(self.status, VerificationStatusEnum::Queued) + } + pub fn is_verification_success(&self) -> bool { + matches!(self.status, VerificationStatusEnum::Successful) + } +} + +#[derive(Debug, Serialize)] +pub struct VerifyContractRequest { + #[serde(rename = "contractAddress")] + contract_address: String, + #[serde(rename = "sourceCode")] + source_code: StandardJsonCompilerInput, + #[serde(rename = "codeFormat")] + code_format: String, + #[serde(rename = "contractName")] + contract_name: String, + #[serde(rename = "compilerSolcVersion")] + compiler_version: String, + #[serde(rename = "compilerZksolcVersion")] + zk_compiler_version: String, + #[serde(rename = "constructorArguments")] + constructor_arguments: String, + #[serde(rename = "optimizationUsed")] + optimization_used: bool, +} diff --git a/crates/verify/src/zksync/standard_json.rs b/crates/verify/src/zksync/standard_json.rs new file mode 100644 index 000000000..9509a36c6 --- /dev/null +++ b/crates/verify/src/zksync/standard_json.rs @@ -0,0 +1,37 @@ +use super::{VerifyArgs, ZksyncSourceProvider}; +use crate::zk_provider::ZkVerificationContext; +use eyre::{Context, Result}; +use foundry_compilers::zksolc::input::StandardJsonCompilerInput; + +#[derive(Debug)] +pub struct ZksyncStandardJsonSource; + +impl ZksyncSourceProvider for ZksyncStandardJsonSource { + fn zk_source( + &self, + _args: &VerifyArgs, + context: &ZkVerificationContext, + ) -> Result<(StandardJsonCompilerInput, String)> { + let input = foundry_compilers::zksync::project_standard_json_input( + &context.project, + &context.target_path, + ) + .wrap_err("failed to get zksolc standard json")?; + + // Extract the path relative to the project root + let relative_path = context + .target_path + .strip_prefix(context.project.root()) + .unwrap_or(context.target_path.as_path()) + .display() + .to_string(); + + // Ensure the path uses forward slashes consistently (handles Windows paths) + let normalized_path = relative_path.replace("\\", "/"); + + // Format the name as /: + let name = format!("{}:{}", normalized_path, context.target_name); + + Ok((input, name)) + } +}