Skip to content

Commit

Permalink
fix: resolves import paths issue (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
dutterbutter authored Oct 27, 2023
1 parent ed8864a commit 8b442c4
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 29 deletions.
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

0 comments on commit 8b442c4

Please sign in to comment.