diff --git a/README.md b/README.md index ebc861ef5..d3697b585 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Please note that `foundry-zksync` is still in its **alpha** stage. Some features | ✅ Features | 🚫 Limitations | |------------------------------------------------------------------------------------------------|------------------------------------------------------------------------| -| Compile smart contracts with the [zksolc compiler](https://github.com/matter-labs/zksolc-bin). | Must use relative import paths based on compiled source code location. | +| Compile smart contracts with the [zksolc compiler](https://github.com/matter-labs/zksolc-bin). | Can't find `test/` directory | | Deploy smart contracts to zkSync Era mainnet, testnet, or local test node. | `script` command lacks `zksolc` support. | | Bridge assets L1 <-> L2. | Cheat codes are not supported. | | Call deployed contracts on zkSync Era testnet or local test node. | Lacks advanced testing methods (e.g., variant testing). | @@ -95,20 +95,9 @@ $ tree . -d -L 1 ├── test ``` -Due to a known issue where import paths need to be relative, it's necessary to modify certain paths in the `hello-foundry-zksync` project before compiling. This ensures proper resolution of dependencies during compilation. The required changes are outlined below: +#### Compiling contracts -**Modifications:** -1. In the file `lib/forge-std/src/StdAssertions.sol`, adjust the import statement as follows: - ```solidity - import {DSTest} from "../lib/ds-test/src/test.sol"; - ``` - -2. In the file `lib/forge-std/src/Test.sol`, update the import path in a similar manner: - ```solidity - import {DSTest} from "../lib/ds-test/src/test.sol"; - ``` - -We can then build the project with zkforge zkbuild: +We can build the project with zkforge zkbuild: ``` $ zkforge zkbuild Compiling smart contracts... @@ -125,7 +114,11 @@ Compiled Successfully #### Running Tests -You can run the tests using `zkforge test`. The command and its expected output are shown below: +You can run the tests using `zkforge test`. + +>❗Known issue of not being able to find tests in the `/tests/` directory. + +The command and its expected output are shown below: ```bash $ zkforge test diff --git a/crates/zkforge/bin/cmd/test/mod.rs b/crates/zkforge/bin/cmd/test/mod.rs index 04dc6b6fa..574145c19 100644 --- a/crates/zkforge/bin/cmd/test/mod.rs +++ b/crates/zkforge/bin/cmd/test/mod.rs @@ -176,6 +176,7 @@ impl TestArgs { compiler_path: zksolc_manager.get_full_compiler_path(), is_system: false, force_evmla: false, + remappings: config.remappings.clone(), }; let mut zksolc = ZkSolc::new(zksolc_opts, project); diff --git a/crates/zkforge/bin/cmd/zk_build.rs b/crates/zkforge/bin/cmd/zk_build.rs index 1711ec364..703986f1e 100644 --- a/crates/zkforge/bin/cmd/zk_build.rs +++ b/crates/zkforge/bin/cmd/zk_build.rs @@ -33,7 +33,7 @@ use super::{ }, }; use clap::Parser; -use ethers::prelude::Project; +use ethers::{prelude::Project, solc::remappings::RelativeRemapping}; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_config::{ figment::{ @@ -156,7 +156,9 @@ impl ZkBuildArgs { let zksolc_manager = self.setup_zksolc_manager()?; - self.compile_smart_contracts(zksolc_manager, project) + let remappings = config.remappings; + + self.compile_smart_contracts(zksolc_manager, project, remappings) } /// Returns whether `ZkBuildArgs` was configured with `--watch` pub fn _is_watch(&self) -> bool { @@ -220,11 +222,13 @@ impl ZkBuildArgs { &self, zksolc_manager: ZkSolcManager, project: Project, + remappings: Vec, ) -> eyre::Result<()> { let zksolc_opts = ZkSolcOpts { compiler_path: zksolc_manager.get_full_compiler_path(), is_system: self.is_system, force_evmla: self.force_evmla, + remappings, }; let mut zksolc = ZkSolc::new(zksolc_opts, project); diff --git a/crates/zkforge/bin/cmd/zk_solc.rs b/crates/zkforge/bin/cmd/zk_solc.rs index 1fc0f3d9e..0a038df60 100644 --- a/crates/zkforge/bin/cmd/zk_solc.rs +++ b/crates/zkforge/bin/cmd/zk_solc.rs @@ -31,7 +31,7 @@ /// construct the path and file for saving the compiler output artifacts. use ansi_term::Colour::{Red, Yellow}; use ethers::{ - prelude::{artifacts::Source, Solc}, + prelude::{artifacts::Source, remappings::RelativeRemapping, Solc}, solc::{ artifacts::{ output_selection::FileOutputSelection, CompactBytecode, CompactDeployedBytecode, @@ -43,6 +43,7 @@ use ethers::{ types::Bytes, }; use eyre::{Context, ContextCompat, Result}; +use regex::Regex; use semver::Version; use serde::Deserialize; use serde_json::Value; @@ -60,6 +61,7 @@ pub struct ZkSolcOpts { pub compiler_path: PathBuf, pub is_system: bool, pub force_evmla: bool, + pub remappings: Vec, } /// Files that should be compiled with a given solidity version. @@ -119,6 +121,7 @@ pub struct ZkSolc { is_system: bool, force_evmla: bool, standard_json: Option, + remappings: Vec, } impl fmt::Display for ZkSolc { @@ -143,6 +146,7 @@ impl ZkSolc { is_system: opts.is_system, force_evmla: opts.force_evmla, standard_json: None, + remappings: opts.remappings, } } @@ -228,9 +232,7 @@ impl ZkSolc { // Step 2: Compile Contracts for Each Source for (solc, version) in sources { //configure project solc for each solc version - for source in version.1 { - // Contract path is an absolute path of the file. - let contract_path = source.0.clone(); + for (contract_path, _) in version.1 { // Check if the contract_path is in 'sources' directory or its subdirectories let is_in_sources_dir = contract_path .ancestors() @@ -248,7 +250,7 @@ impl ZkSolc { ))?; // Step 4: Build Compiler Arguments - let comp_args = self.build_compiler_args(&source, &solc); + let comp_args = self.build_compiler_args(&contract_path, &solc); // Step 5: Run Compiler and Handle Output let mut cmd = Command::new(&self.compiler_path); @@ -268,6 +270,13 @@ impl ZkSolc { let output = child.wait_with_output().wrap_err("Could not run compiler cmd")?; if !output.status.success() { + // Skip this file if the compiler output is empty + // currently zksolc returns false for success if output is empty + // when output is empty, it has a length of 3, `[]\n` + // solc returns true for success if output is empty + if output.stderr.len() <= 3 { + continue; + } eyre::bail!( "Compilation failed with {:?}. Using compiler: {:?}, with args {:?} {:?}", String::from_utf8(output.stderr).unwrap_or_default(), @@ -320,11 +329,7 @@ impl ZkSolc { /// # Returns /// /// A vector of strings representing the compiler arguments. - fn build_compiler_args( - &self, - versioned_source: &(PathBuf, Source), - solc: &Solc, - ) -> Vec { + fn build_compiler_args(&self, contract_path: &Path, solc: &Solc) -> Vec { // Get the solc compiler path as a string let solc_path = solc.solc.to_str().expect("Error configuring solc compiler.").to_string(); @@ -332,7 +337,7 @@ impl ZkSolc { let mut comp_args = vec!["--standard-json".to_string(), "--solc".to_string(), solc_path]; // Check if system mode is enabled or if the source path contains "is-system" - if self.is_system || versioned_source.0.to_str().unwrap().contains("is-system") { + if self.is_system || contract_path.to_str().unwrap().contains("is-system") { comp_args.push("--system-mode".to_string()); } @@ -643,12 +648,18 @@ impl ZkSolc { .insert("*".to_string(), file_output_selection.clone()); // Step 4: Generate Standard JSON Input - let standard_json = self + let mut standard_json = self .project .standard_json_input(contract_path) .wrap_err("Could not get standard json input") .unwrap(); + // Apply remappings for each contract dependency + for (_path, _source) in &mut standard_json.sources { + remap_source_path(_path, &self.remappings); + _source.content = self.remap_source_content(_source.content.to_string()).into(); + } + // Store the generated standard JSON input in the ZkSolc instance self.standard_json = Some(standard_json.to_owned()); @@ -796,6 +807,93 @@ impl ZkSolc { fs::create_dir_all(&path).wrap_err("Could not create artifacts directory")?; Ok(path) } + + fn remap_source_content(&mut self, source_content: String) -> String { + let content = source_content; + + // Get relative remappings + let remappings = &self.remappings; + + // Replace imports with placeholders + let content = replace_imports_with_placeholders(content, remappings); + + substitute_remapped_paths(content, remappings) + } +} +// TODO: +// This approach will need to be refactored and improved +// It solves the import path issue but should be revisited before production +fn replace_imports_with_placeholders(content: String, remappings: &[RelativeRemapping]) -> String { + let mut replaced_content = content; + + // Iterate through the remappings + for (i, remapping) in remappings.iter().enumerate() { + let placeholder = format!("REMAP_PLACEHOLDER_{}", i); + + // Define a pattern that matches the import statement, capturing the rest of the path + let pattern = format!( + r#"import\s+((?:\{{.*?\}}\s+from\s+)?)\s*"{}(?P[^"]*)""#, + regex::escape(&remapping.name) + ); + + let replacement = format!(r#"import {}"{}$rest""#, "$1", placeholder); + + replaced_content = + Regex::new(&pattern).unwrap().replace_all(&replaced_content, replacement).into_owned(); + } + + replaced_content +} +// TODO: +// This approach will need to be refactored and improved +// It solves the import path issue but should be revisited before production +fn substitute_remapped_paths(content: String, remappings: &[RelativeRemapping]) -> String { + let mut substituted = content; + + loop { + let mut made_replacements = false; + + for (i, r) in remappings.iter().enumerate() { + let placeholder = format!("REMAP_PLACEHOLDER_{}", i); + let import_path = r.path.path.to_str().unwrap(); + + let new_substituted = substituted.replace(&placeholder, import_path); + + if new_substituted != substituted { + made_replacements = true; + substituted = new_substituted; + } + } + + // Exit the loop if no more replacements were made + if !made_replacements { + break; + } + } + + substituted +} +// TODO: +// This approach will need to be refactored and improved +// It solves the import path issue but should be revisited before production +fn remap_source_path(source_path: &mut PathBuf, remappings: &[RelativeRemapping]) { + let source_path_str = source_path.to_str().expect("Failed to convert path to str"); + + for r in remappings.iter() { + let prefix = &r.name; + + let mut parts = source_path_str.splitn(2, prefix); + + if let Some(_before) = parts.next() { + if let Some(after) = parts.next() { + let temp_path = r.path.path.join(after); + + *source_path = + PathBuf::from(temp_path.to_str().unwrap().replace("src/src/", "src/")); + break; + } + } + } } #[derive(Debug, Deserialize)]