Skip to content

Commit

Permalink
feat(verify): zksync contract verification (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
Karrq authored Jul 3, 2024
1 parent 6e1c282 commit 21c0062
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 6 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/forge/bin/cmd/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ impl CreateArgs {
via_ir: self.opts.via_ir,
evm_version: self.opts.compiler.evm_version,
show_standard_json_input: self.show_standard_json_input,
zksync: self.opts.compiler.zk.enabled(),
};

// Check config for Etherscan API Keys to avoid preflight check failing if no
Expand Down Expand Up @@ -539,7 +540,7 @@ impl CreateArgs {
.constructor()
.ok_or_else(|| eyre::eyre!("could not find constructor"))?
.abi_encode_input(&args)?;
constructor_args = Some(hex::encode(encoded_args));
constructor_args = Some(hex::encode_prefixed(encoded_args));
}

self.verify_preflight_check(contract, constructor_args.clone(), chain).await?;
Expand Down Expand Up @@ -589,6 +590,7 @@ impl CreateArgs {
via_ir: self.opts.via_ir,
evm_version: self.opts.compiler.evm_version,
show_standard_json_input: self.show_standard_json_input,
zksync: self.opts.compiler.zk.enabled(),
};
println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier);
verify.run().await.map(|_| address)
Expand Down
4 changes: 4 additions & 0 deletions crates/forge/bin/cmd/script/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct VerifyBundle {
pub retry: RetryArgs,
pub verifier: VerifierArgs,
pub via_ir: bool,
pub zksync: bool,
}

impl VerifyBundle {
Expand Down Expand Up @@ -46,6 +47,7 @@ impl VerifyBundle {
};

let via_ir = config.via_ir;
let zksync = config.zksync.should_compile();

VerifyBundle {
num_of_optimizations,
Expand All @@ -55,6 +57,7 @@ impl VerifyBundle {
retry,
verifier,
via_ir,
zksync,
}
}

Expand Down Expand Up @@ -116,6 +119,7 @@ impl VerifyBundle {
via_ir: self.via_ir,
evm_version: None,
show_standard_json_input: false,
zksync: self.zksync,
};

return Some(verify)
Expand Down
87 changes: 87 additions & 0 deletions crates/forge/bin/cmd/verify/etherscan/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ use eyre::{Context, Result};
use foundry_block_explorers::verify::CodeFormat;
use foundry_compilers::{
artifacts::{BytecodeHash, Source},
zksync::{
artifacts::{BytecodeHash as ZkBytecodeHash, CompilerInput as ZkCompilerInput},
compile::{output::AggregatedCompilerOutput as ZkAggregatedCompilerOutput, ZkSolc},
},
AggregatedCompilerOutput, CompilerInput, Project, Solc,
};
use semver::{BuildMetadata, Version};

use std::{collections::BTreeMap, path::Path};

#[derive(Debug)]
Expand Down Expand Up @@ -43,6 +48,39 @@ impl EtherscanSourceProvider for EtherscanFlattenedSource {
let name = args.contract.name.clone();
Ok((source, name, CodeFormat::SingleFile))
}

fn zk_source(
&self,
args: &VerifyArgs,
project: &Project,
target: &Path,
version: &Version,
) -> Result<(String, String, CodeFormat)> {
let metadata = project.zksync_zksolc_config.settings.metadata.as_ref();
let bch = metadata.and_then(|m| m.bytecode_hash).unwrap_or_default();

eyre::ensure!(
bch == ZkBytecodeHash::Keccak256,
"When using flattened source with zksync, bytecodeHash must be set to keccak256 because Etherscan uses Keccak256 in its Compiler Settings when re-compiling your code. BytecodeHash is currently: {}. Hint: Set the bytecodeHash key in your foundry.toml :)",
bch,
);

let source = project.flatten(target).wrap_err("Failed to flatten contract")?;

if !args.force {
// solc dry run of flattened code
self.zk_check_flattened(source.clone(), version, target).map_err(|err| {
eyre::eyre!(
"Failed to compile the flattened code locally: `{}`\
To skip this solc dry, have a look at the `--force` flag of this command.",
err
)
})?;
}

let name = args.contract.name.clone();
Ok((source, name, CodeFormat::SingleFile))
}
}

impl EtherscanFlattenedSource {
Expand Down Expand Up @@ -94,6 +132,55 @@ Diagnostics: {diags}",

Ok(())
}

/// Attempts to compile the flattened content locally with the zksolc compiler version.
///
/// This expects the completely flattened `content´ and will try to compile it using the
/// provided compiler. If the compiler is missing it will be installed.
///
/// # Errors
///
/// If it failed to install a missing solc compiler
///
/// # Exits
///
/// If the solc compiler output contains errors, this could either be due to a bug in the
/// flattening code or could to conflict in the flattened code, for example if there are
/// multiple interfaces with the same name.
fn zk_check_flattened(
&self,
content: impl Into<String>,
version: &Version,
contract_path: &Path,
) -> Result<()> {
let version = strip_build_meta(version.clone());
let zksolc = ZkSolc::find_installed_version(&version)?
.unwrap_or(ZkSolc::blocking_install(&version)?);

let input = ZkCompilerInput {
language: "Solidity".to_string(),
sources: BTreeMap::from([("contract.sol".into(), Source::new(content))]),
settings: Default::default(),
};

let (out, _) = zksolc.compile(&input)?;
if out.has_error() {
let mut o = ZkAggregatedCompilerOutput::default();
o.extend(version, out);
let diags = o.diagnostics(&[], &[], Default::default());

eyre::bail!(
"\
Failed to compile the flattened code locally.
This could be a bug, please inspect the output of `forge flatten {}` and report an issue.
To skip this zksolc dry, pass `--force`.
Diagnostics: {diags}",
contract_path.display()
);
}

Ok(())
}
}

/// Strips [BuildMetadata] from the [Version]
Expand Down
114 changes: 111 additions & 3 deletions crates/forge/bin/cmd/verify/etherscan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ use futures::FutureExt;
use once_cell::sync::Lazy;
use regex::Regex;
use semver::{BuildMetadata, Version};

use std::{
fmt::Debug,
path::{Path, PathBuf},
str::FromStr,
};

mod flatten;
Expand All @@ -48,6 +50,14 @@ trait EtherscanSourceProvider: Send + Sync + Debug {
target: &Path,
version: &Version,
) -> Result<(String, String, CodeFormat)>;

fn zk_source(
&self,
args: &VerifyArgs,
project: &Project,
target: &Path,
version: &Version,
) -> Result<(String, String, CodeFormat)>;
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -172,6 +182,10 @@ impl VerificationProvider for EtherscanVerificationProvider {
return Err(eyre!("Verification is still pending...",))
}

if resp.result == "In progress" {
return Err(eyre!("Verification is in progress...",))
}

if resp.result == "Unable to verify" {
return Err(eyre!("Unable to verify.",))
}
Expand Down Expand Up @@ -242,6 +256,7 @@ impl EtherscanVerificationProvider {
/// Configures the API request to the etherscan API using the given [`VerifyArgs`].
async fn prepare_request(&mut self, args: &VerifyArgs) -> Result<(Client, VerifyContract)> {
let config = args.try_load_config_emit_warnings()?;

let etherscan = self.client(
args.etherscan.chain.unwrap_or_default(),
args.verifier.verifier_url.as_deref(),
Expand Down Expand Up @@ -327,16 +342,39 @@ impl EtherscanVerificationProvider {
let project = config.project()?;

let contract_path = self.contract_path(args, &project)?;
let compiler_version = self.compiler_version(args, &config, &project)?;
let (source, contract_name, code_format) =
self.source_provider(args).source(args, &project, &contract_path, &compiler_version)?;
let mut compiler_version = self.compiler_version(args, &config, &project)?;
let zk_compiler_version = self.zk_compiler_version(args, &config, &project)?;

let source_provider = self.source_provider(args);
let (source, contract_name, code_format) = if let Some(zk) = &zk_compiler_version {
source_provider.zk_source(args, &project, &contract_path, &zk.zksolc)
} else {
source_provider.source(args, &project, &contract_path, &compiler_version)
}?;

let zk_args = match zk_compiler_version {
None => vec![],
Some(zk) => {
if let Some(solc) = zk.solc {
compiler_version = Version::new(solc.major, solc.minor, solc.patch);
}

let compiler_mode = if zk.is_zksync_solc { "zksync" } else { "solc" }.to_string();

vec![
("compilermode".to_string(), compiler_mode),
("zksolcVersion".to_string(), format!("v{}", zk.zksolc)),
]
}
};
let compiler_version = format!("v{}", ensure_solc_build_metadata(compiler_version).await?);

let constructor_args = self.constructor_args(args, &project)?;
let mut verify_args =
VerifyContract::new(args.address, contract_name, source, compiler_version)
.constructor_arguments(constructor_args)
.code_format(code_format);
verify_args.other.extend(zk_args.into_iter());

if args.via_ir {
// we explicitly set this __undocumented__ argument to true if provided by the user,
Expand Down Expand Up @@ -379,6 +417,69 @@ impl EtherscanVerificationProvider {
Ok(path)
}

fn zk_compiler_version(
&mut self,
args: &VerifyArgs,
config: &Config,
project: &Project,
) -> Result<Option<ZkVersion>> {
if !args.zksync {
return Ok(None);
}

//TODO: remove when foundry-compilers zksolc detection is fixed for 1.5.0
let get_zksolc_compiler_version = |path: &std::path::Path| -> Result<Version> {
use std::process::*;
let mut cmd = Command::new(path);
cmd.arg("--version")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped());
debug!(?cmd, "getting ZkSolc version");
let output = cmd.output().wrap_err("error retrieving --version for zksolc")?;

if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let version = stdout
.lines()
.filter(|l| !l.trim().is_empty())
.last()
.ok_or(eyre!("Version not found in zksolc output"))?;
Ok(Version::from_str(
version
.split_whitespace()
.find(|s| s.starts_with('v'))
.ok_or(eyre!("Unable to retrieve version from zksolc output"))?
.trim_start_matches('v'),
)?)
} else {
Err(eyre!("zkSolc error: {}", String::from_utf8_lossy(&output.stderr)))
.wrap_err("Error retrieving zksolc version with --version")
}
};

let zksolc = get_zksolc_compiler_version(project.zksync_zksolc.zksolc.as_ref())?;
let mut is_zksync_solc = false;

let solc = if let Some(solc) = &config.zksync.solc_path {
let solc = Solc::new(solc);
let version = solc.version().wrap_err(
"unable to retrieve version of solc in use with zksolc via `solc_path`",
)?;
//TODO: determine if this solc is zksync or not
Some(version)
} else {
//if there's no `solc_path` specified then we use the same
// as the project version, but the zksync forc
is_zksync_solc = true;
Some(project.solc.version().wrap_err(
"unable to retrieve version of solc in use with zksolc via project `solc`",
)?)
};

Ok(Some(ZkVersion { zksolc, solc, is_zksync_solc }))
}

/// Parse the compiler version.
/// The priority desc:
/// 1. Through CLI arg `--compiler-version`
Expand Down Expand Up @@ -482,6 +583,13 @@ async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
}
}

#[derive(Debug)]
pub struct ZkVersion {
zksolc: Version,
solc: Option<Version>,
is_zksync_solc: bool,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
36 changes: 35 additions & 1 deletion crates/forge/bin/cmd/verify/etherscan/standard_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use eyre::{Context, Result};
use foundry_block_explorers::verify::CodeFormat;
use foundry_compilers::{artifacts::StandardJsonCompilerInput, Project};
use semver::Version;

use std::path::Path;

#[derive(Debug)]
Expand Down Expand Up @@ -34,7 +35,40 @@ impl EtherscanSourceProvider for EtherscanStandardJsonSource {
let source =
serde_json::to_string(&input).wrap_err("Failed to parse standard json input")?;

trace!(target: "forge::verify", standard_json=source, "determined standard json input");
trace!(target: "forge::verify", standard_json=?source, "determined standard json input");

let name = format!(
"{}:{}",
target.strip_prefix(project.root()).unwrap_or(target).display(),
args.contract.name.clone()
);
Ok((source, name, CodeFormat::StandardJsonInput))
}

fn zk_source(
&self,
args: &VerifyArgs,
project: &Project,
target: &Path,
version: &Version,
) -> Result<(String, String, CodeFormat)> {
let mut input = project
.zksync_standard_json_input(target)
.wrap_err("Failed to get standard json input")?
.normalize_evm_version(version);

input.settings.libraries.libs = input
.settings
.libraries
.libs
.into_iter()
.map(|(f, libs)| (f.strip_prefix(project.root()).unwrap_or(&f).to_path_buf(), libs))
.collect();

let source =
serde_json::to_string(&input).wrap_err("Failed to parse standard json input")?;

trace!(target: "forge::verify", standard_json=?source, "determined zksync standard json input");

let name = format!(
"{}:{}",
Expand Down
Loading

0 comments on commit 21c0062

Please sign in to comment.