Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support error / warning suppression in zksolc #33

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions crates/compilers/src/compilers/zksolc/input.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use super::{settings::ZkSolcSettings, ZkSettings};
use super::{
settings::{ZkSolcError, ZkSolcSettings, ZkSolcWarning},
ZkSettings,
};
use crate::{
compilers::{solc::SolcLanguage, CompilerInput},
solc,
Expand All @@ -8,6 +11,7 @@ use semver::Version;
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::HashSet,
path::{Path, PathBuf},
};

Expand All @@ -33,7 +37,7 @@ impl CompilerInput for ZkSolcVersionedInput {
version: Version,
) -> Self {
let ZkSolcSettings { settings, cli_settings } = settings;
let input = ZkSolcInput { language, sources, settings }.sanitized(&version);
let input = ZkSolcInput::new(language, sources, settings).sanitized(&version);

Self { solc_version: version, input, cli_settings }
}
Expand Down Expand Up @@ -65,10 +69,18 @@ impl CompilerInput for ZkSolcVersionedInput {

/// Input type `zksolc` expects.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ZkSolcInput {
pub language: SolcLanguage,
pub sources: Sources,
pub settings: ZkSettings,
// For `zksolc` versions <1.5.7, suppressed warnings / errors were specified on the same level
// as `settings`. For `zksolc` 1.5.7+, they are specified inside `settings`. Since we want to
// support both options at the time, we duplicate fields from `settings` here.
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
Karrq marked this conversation as resolved.
Show resolved Hide resolved
pub suppressed_warnings: HashSet<ZkSolcWarning>,
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub suppressed_errors: HashSet<ZkSolcError>,
}

/// Default `language` field is set to `"Solidity"`.
Expand All @@ -78,11 +90,19 @@ impl Default for ZkSolcInput {
language: SolcLanguage::Solidity,
sources: Sources::default(),
settings: ZkSettings::default(),
suppressed_warnings: HashSet::default(),
suppressed_errors: HashSet::default(),
}
}
}

impl ZkSolcInput {
fn new(language: SolcLanguage, sources: Sources, settings: ZkSettings) -> Self {
let suppressed_warnings = settings.suppressed_warnings.clone();
let suppressed_errors = settings.suppressed_errors.clone();
Self { language, sources, settings, suppressed_warnings, suppressed_errors }
}

/// Removes the `base` path from all source files
pub fn strip_prefix(&mut self, base: impl AsRef<Path>) {
let base = base.as_ref();
Expand Down
3 changes: 1 addition & 2 deletions crates/compilers/src/compilers/zksolc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ use std::os::unix::fs::PermissionsExt;

pub mod input;
pub mod settings;
pub use settings::ZkSettings;
pub use settings::ZkSolcSettings;
pub use settings::{ZkSettings, ZkSolcSettings};

pub const ZKSOLC: &str = "zksolc";
pub const ZKSYNC_SOLC_RELEASE: Version = Version::new(1, 0, 1);
Expand Down
34 changes: 32 additions & 2 deletions crates/compilers/src/compilers/zksolc/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ use foundry_compilers_artifacts::{
use semver::Version;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeSet,
collections::{BTreeSet, HashSet},
fmt,
path::{Path, PathBuf},
str::FromStr,
};

///
/// The Solidity compiler codegen.
///
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Codegen {
Expand All @@ -28,6 +27,25 @@ pub enum Codegen {
EVMLA,
}

/// `zksolc` warnings that can be suppressed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
hedgar2017 marked this conversation as resolved.
Show resolved Hide resolved
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ZkSolcWarning {
/// `txorigin` warning: Using `tx.origin` in place of `msg.sender`.
TxOrigin,
}

/// `zksolc` errors that can be suppressed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ZkSolcError {
/// `sendtransfer` error: Using `send()` or `transfer()` methods on `address payable` instead
/// of `call()`.
SendTransfer,
}

/// zksolc standard json input settings. See:
/// https://docs.zksync.io/zk-stack/components/compiler/toolchain/solidity.html#standard-json for differences
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -82,6 +100,12 @@ pub struct ZkSettings {
/// Whether to compile via EVM assembly.
#[serde(default, rename = "forceEVMLA")]
pub force_evmla: bool,
/// Suppressed `zksolc` warnings.
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub suppressed_warnings: HashSet<ZkSolcWarning>,
Copy link

@elfedy elfedy Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively these fields could go on ZkSolcSettings vs here in order to remove duplicating the fields in both ZkSettings and ZkSolcInput (since one struct contains the other this may not be ideal). Then when building the input we pass them directly, we have this flow for CliSettings already

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain how suppressed warnings / errors would be passed in this case? AFAIU, ZkSolcInput covers the entire standard JSON input for zksolc and zksolc requires these fields on the top level (not inside the settings object), so wouldn't it need to have these fields present in any case?

Copy link

@elfedy elfedy Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They need to be in ZkSolcInput but not in ZkSettings neccesarily.

So there are a couple of structs involved in building the compilation input:

  • ZkSolcSettings: which are the actual settings that are passed by foundry to the Project. This for now contains two fields
    • ZkSettings: which aims to be the literal settings object on JSON input.
    • CliSettings: which are cli arguments when compiling (e.g: base_path).
  • ZkSolcVersionedInput : Which is built when resolving solc versions. This has all the information needed to compile.
  • ZkSolcInput, the input actually passed as json

So lets say they are in ZkSolcSettings , when building the ZkSolcVersionedInput we would have:

impl CompilerInput for ZkSolcVersionedInput {
     type Settings = ZkSolcSettings;
     ...
    fn build(
        sources: Sources,
        settings: Self::Settings,
        language: Self::Language,
        version: Version,
    ) -> Self {

        let ZkSolcSettings { settings, cli_settings, suppressed_warnings, suppressed_errors } = settings;
        let input = ZkSolcInput { language, sources, settings, suppressed_warnings, suppressed_errors }.sanitized(&version);

        Self { solc_version: version, input, cli_settings }
    }
...
}

Hope that makes sense

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, I should not have put any new settings outside of the settings field.
Would it help if I deprecated them where they currently are and moved them inside settings?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hedgar2017 that does seem cleaner

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elfedy please try this one!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@slowli @hedgar2017 tested the new release here: b04a920 (Replace <PATH_TO_RELEASE> with the local path to zksolc test release). Seems to be working correctly (Tests pass with it and fail with previous releases)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can wait until those changes are part of a stable release then bump the version here and incorporate the changes in that test commit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elfedy great!
We're going to make a release in the next few days.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elfedy released with zksolc v1.5.7!
Also supported by zksolc v1.5.6 and older, but one level above the settings object.

/// Suppressed `zksolc` errors.
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub suppressed_errors: HashSet<ZkSolcError>,
}

// Analogous to SolcSettings for Zk compiler
Expand Down Expand Up @@ -160,6 +184,8 @@ impl Default for ZkSettings {
llvm_options: Default::default(),
force_evmla: false,
codegen: Default::default(),
suppressed_errors: Default::default(),
suppressed_warnings: Default::default(),
}
}
}
Expand All @@ -186,6 +212,8 @@ impl CompilerSettings for ZkSolcSettings {
llvm_options,
force_evmla,
codegen,
suppressed_warnings,
suppressed_errors,
},
..
} = self;
Expand All @@ -202,6 +230,8 @@ impl CompilerSettings for ZkSolcSettings {
&& *llvm_options == other.settings.llvm_options
&& *force_evmla == other.settings.force_evmla
&& *codegen == other.settings.codegen
&& *suppressed_warnings == other.settings.suppressed_warnings
&& *suppressed_errors == other.settings.suppressed_errors
}

fn with_remappings(mut self, remappings: &[Remapping]) -> Self {
Expand Down
128 changes: 126 additions & 2 deletions crates/compilers/tests/zksync.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr};
use std::{
collections::{HashMap, HashSet},
fs,
path::PathBuf,
str::FromStr,
};

use foundry_compilers::{
buildinfo::BuildInfo,
cache::CompilerCache,
project_util::*,
resolver::parse::SolData,
zksolc::{input::ZkSolcInput, ZkSolcCompiler, ZkSolcSettings},
zksolc::{
input::ZkSolcInput,
settings::{ZkSolcError, ZkSolcWarning},
ZkSolc, ZkSolcCompiler, ZkSolcSettings,
},
zksync::{self, artifact_output::zk::ZkArtifactOutput},
Graph, ProjectBuilder, ProjectPathsConfig,
};
Expand Down Expand Up @@ -42,6 +51,121 @@ fn zksync_can_compile_dapp_sample() {
assert_eq!(cache, updated_cache);
}

fn test_zksync_can_compile_contract_with_suppressed_errors(compiler: ZkSolcCompiler) {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init()
.ok();
let mut project = TempProject::<ZkSolcCompiler, ZkArtifactOutput>::dapptools().unwrap();
project.project_mut().compiler = compiler;

project
.add_source(
"Erroneous",
r#"
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.10;
contract Erroneous {
function distribute(address payable recipient) public {
recipient.send(1);
recipient.transfer(1);
}
}
"#,
)
.unwrap();

let compiled = zksync::project_compile(project.project()).unwrap();
assert!(compiled.has_compiler_errors());

project.project_mut().settings.settings.suppressed_errors =
HashSet::from([ZkSolcError::SendTransfer]);

let compiled = zksync::project_compile(project.project()).unwrap();
compiled.assert_success();
assert!(compiled.find_first("Erroneous").is_some());
}

#[test]
fn zksync_can_compile_contract_with_suppressed_errors() {
test_zksync_can_compile_contract_with_suppressed_errors(ZkSolcCompiler::default());
}

#[test]
fn zksync_pre_1_5_7_can_compile_contract_with_suppressed_errors() {
let compiler = ZkSolcCompiler {
zksolc: ZkSolc::get_path_for_version(&semver::Version::new(1, 5, 6)).unwrap(),
solc: Default::default(),
};
test_zksync_can_compile_contract_with_suppressed_errors(compiler);
}

fn test_zksync_can_compile_contract_with_suppressed_warnings(compiler: ZkSolcCompiler) {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init()
.ok();
let mut project = TempProject::<ZkSolcCompiler, ZkArtifactOutput>::dapptools().unwrap();
project.project_mut().compiler = compiler;

project
.add_source(
"Warning",
r#"
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.10;
contract Warning {
function test() public view {
require(tx.origin != address(0), "what");
}
}
"#,
)
.unwrap();

let compiled = zksync::project_compile(project.project()).unwrap();
compiled.assert_success();
assert!(
compiled
.compiler_output
.errors
.iter()
.any(|err| err.is_warning() && err.message.contains("tx.origin")),
"{:#?}",
compiled.compiler_output.errors
);

project.project_mut().settings.settings.suppressed_warnings =
HashSet::from([ZkSolcWarning::TxOrigin]);

let compiled = zksync::project_compile(project.project()).unwrap();
compiled.assert_success();
assert!(compiled.find_first("Warning").is_some());
assert!(
!compiled
.compiler_output
.errors
.iter()
.any(|err| err.is_warning() && err.message.contains("tx.origin")),
"{:#?}",
compiled.compiler_output.errors
);
}

#[test]
fn zksync_can_compile_contract_with_suppressed_warnings() {
test_zksync_can_compile_contract_with_suppressed_warnings(ZkSolcCompiler::default());
}

#[test]
fn zksync_pre_1_5_7_can_compile_contract_with_suppressed_warnings() {
let compiler = ZkSolcCompiler {
zksolc: ZkSolc::get_path_for_version(&semver::Version::new(1, 5, 6)).unwrap(),
solc: Default::default(),
};
test_zksync_can_compile_contract_with_suppressed_warnings(compiler);
}

#[test]
fn zksync_can_compile_dapp_detect_changes_in_libs() {
let _ = tracing_subscriber::fmt()
Expand Down