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

fix: resolves import paths issue #133

Merged
merged 2 commits into from
Oct 27, 2023
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
23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down Expand Up @@ -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...
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/zkforge/bin/cmd/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions crates/zkforge/bin/cmd/zk_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -220,11 +222,13 @@ impl ZkBuildArgs {
&self,
zksolc_manager: ZkSolcManager,
project: Project,
remappings: Vec<RelativeRemapping>,
) -> 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);
Expand Down
122 changes: 110 additions & 12 deletions crates/zkforge/bin/cmd/zk_solc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -60,6 +61,7 @@ pub struct ZkSolcOpts {
pub compiler_path: PathBuf,
pub is_system: bool,
pub force_evmla: bool,
pub remappings: Vec<RelativeRemapping>,
}

/// Files that should be compiled with a given solidity version.
Expand Down Expand Up @@ -119,6 +121,7 @@ pub struct ZkSolc {
is_system: bool,
force_evmla: bool,
standard_json: Option<StandardJsonCompilerInput>,
remappings: Vec<RelativeRemapping>,
}

impl fmt::Display for ZkSolc {
Expand All @@ -143,6 +146,7 @@ impl ZkSolc {
is_system: opts.is_system,
force_evmla: opts.force_evmla,
standard_json: None,
remappings: opts.remappings,
}
}

Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand All @@ -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(),
Expand Down Expand Up @@ -320,19 +329,15 @@ impl ZkSolc {
/// # Returns
///
/// A vector of strings representing the compiler arguments.
fn build_compiler_args(
&self,
versioned_source: &(PathBuf, Source),
solc: &Solc,
) -> Vec<String> {
fn build_compiler_args(&self, contract_path: &Path, solc: &Solc) -> Vec<String> {
// Get the solc compiler path as a string
let solc_path = solc.solc.to_str().expect("Error configuring solc compiler.").to_string();

// Build compiler arguments
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());
}

Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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<rest>[^"]*)""#,
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)]
Expand Down