Skip to content

Commit

Permalink
fix: create verifiable Standard JSON for projects with external files (
Browse files Browse the repository at this point in the history
…#36)

- Resolves foundry-rs/foundry#5307

Currently, Foundry projects containing Solidity files outside the
project root directory face contract verification failures on block
explorers. This issue occurs from the Standard JSON including unusable
source paths for external files, represented as full absolute paths in
their host file systems.

This PR addresses the issue by improving the path conversion process.
For files not located under the project root directory, relative parent
directory paths (`..`) are used, enabling the compiler to find the files
within the json.

Steps to reproduce the issue are detailed in the linked issue above.
Additionally, a test case representing that scenario has been added.

With this change, the json created in the reproduction scenario will
appear as follows:

```json
{
  "language": "Solidity",
  "sources": {
    "src/Counter.sol": {
      "content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n\nimport \"@remapped/Parent.sol\";\n\ncontract Counter {\n    uint256 public number;\n\n    function setNumber(uint256 newNumber) public {\n        number = newNumber;\n    }\n\n    function increment() public {\n        number++;\n    }\n}\n"
    },
    "../remapped/Parent.sol": {
      "content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\nimport \"./Child.sol\";\n"
    },
    "../remapped/Child.sol": {
      "content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n"
    }
  },
  "settings": {
    "remappings": [
      "@remapped/=../remapped/",
      "ds-test/=lib/forge-std/lib/ds-test/src/",
      "forge-std/=lib/forge-std/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "": [
          "ast"
        ],
        "*": [
          "abi",
          "evm.bytecode",
          "evm.deployedBytecode",
          "evm.methodIdentifiers",
          "metadata"
        ]
      }
    },
    "evmVersion": "paris",
    "libraries": {}
  }
}
```

The source path is now aligned with the project root.

I have successfully deployed and verified the contract on Etherscan
using this change.

`forge create --rpc-url "wss://ethereum-holesky.publicnode.com" --verify
--verifier-url "https://api-holesky.etherscan.io/api"
--etherscan-api-key "..." --private-key "..." src/Counter.sol:Counter`


https://holesky.etherscan.io/address/0xe08c332706185521fc8bc2b224f67adf814b1880#code
  • Loading branch information
tash-2s authored Dec 28, 2023
1 parent b1561d8 commit 247161c
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 11 deletions.
106 changes: 96 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,6 @@ impl<T: ArtifactOutput> Project<T> {
&self,
target: impl AsRef<Path>,
) -> Result<StandardJsonCompilerInput> {
use path_slash::PathExt;

let target = target.as_ref();
tracing::trace!("Building standard-json-input for {:?}", target);
let graph = Graph::resolve(&self.paths)?;
Expand All @@ -514,14 +512,7 @@ impl<T: ArtifactOutput> Project<T> {
let root = self.root();
let sources = sources
.into_iter()
.map(|(path, source)| {
let path: PathBuf = if let Ok(stripped) = path.strip_prefix(root) {
stripped.to_slash_lossy().into_owned().into()
} else {
path.to_slash_lossy().into_owned().into()
};
(path, source.clone())
})
.map(|(path, source)| (rebase_path(root, path), source.clone()))
.collect();

let mut settings = self.solc_config.settings.clone();
Expand Down Expand Up @@ -954,6 +945,62 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
}
}

// Rebases the given path to the base directory lexically.
//
// For instance, given the base `/home/user/project` and the path `/home/user/project/src/A.sol`,
// this function returns `src/A.sol`.
//
// This function transforms a path into a form that is relative to the base directory. The returned
// path starts either with a normal component (e.g., `src`) or a parent directory component (i.e.,
// `..`). It also converts the path into a UTF-8 string and replaces all separators with forward
// slashes (`/`), if they're not.
//
// The rebasing process can be conceptualized as follows:
//
// 1. Remove the leading components from the path that match those in the base.
// 2. Prepend `..` components to the path, matching the number of remaining components in the base.
//
// # Examples
//
// `rebase_path("/home/user/project", "/home/user/project/src/A.sol")` returns `src/A.sol`. The
// common part, `/home/user/project`, is removed from the path.
//
// `rebase_path("/home/user/project", "/home/user/A.sol")` returns `../A.sol`. First, the common
// part, `/home/user`, is removed, leaving `A.sol`. Next, as `project` remains in the base, `..` is
// prepended to the path.
//
// On Windows, paths like `a\b\c` are converted to `a/b/c`.
//
// For more examples, see the test.
fn rebase_path(base: impl AsRef<Path>, path: impl AsRef<Path>) -> PathBuf {
use path_slash::PathExt;

let mut base_components = base.as_ref().components();
let mut path_components = path.as_ref().components();

let mut new_path = PathBuf::new();

while let Some(path_component) = path_components.next() {
let base_component = base_components.next();

if Some(path_component) != base_component {
if base_component.is_some() {
new_path.extend(
std::iter::repeat(std::path::Component::ParentDir)
.take(base_components.count() + 1),
);
}

new_path.push(path_component);
new_path.extend(path_components);

break;
}
}

new_path.to_slash_lossy().into_owned().into()
}

#[cfg(test)]
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
mod tests {
Expand Down Expand Up @@ -1014,4 +1061,43 @@ mod tests {
let contracts = project.compile().unwrap().succeeded().output().contracts;
assert_eq!(contracts.contracts().count(), 2);
}

#[test]
fn can_rebase_path() {
assert_eq!(rebase_path("a/b", "a/b/c"), PathBuf::from("c"));
assert_eq!(rebase_path("a/b", "a/c"), PathBuf::from("../c"));
assert_eq!(rebase_path("a/b", "c"), PathBuf::from("../../c"));

assert_eq!(
rebase_path("/home/user/project", "/home/user/project/A.sol"),
PathBuf::from("A.sol")
);
assert_eq!(
rebase_path("/home/user/project", "/home/user/project/src/A.sol"),
PathBuf::from("src/A.sol")
);
assert_eq!(
rebase_path("/home/user/project", "/home/user/project/lib/forge-std/src/Test.sol"),
PathBuf::from("lib/forge-std/src/Test.sol")
);
assert_eq!(
rebase_path("/home/user/project", "/home/user/A.sol"),
PathBuf::from("../A.sol")
);
assert_eq!(rebase_path("/home/user/project", "/home/A.sol"), PathBuf::from("../../A.sol"));
assert_eq!(rebase_path("/home/user/project", "/A.sol"), PathBuf::from("../../../A.sol"));
assert_eq!(
rebase_path("/home/user/project", "/tmp/A.sol"),
PathBuf::from("../../../tmp/A.sol")
);

assert_eq!(
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/remapped/Child.sol"),
PathBuf::from("../remapped/Child.sol")
);
assert_eq!(
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/verif/../remapped/Parent.sol"),
PathBuf::from("../remapped/Parent.sol")
);
}
}
67 changes: 66 additions & 1 deletion tests/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use foundry_compilers::{
info::ContractInfo,
project_util::*,
remappings::Remapping,
Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project,
utils, Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project,
ProjectCompileOutput, ProjectPathsConfig, Solc, TestFileFilter,
};
use pretty_assertions::assert_eq;
Expand Down Expand Up @@ -1600,6 +1600,71 @@ fn can_sanitize_bytecode_hash() {
assert!(compiled.find_first("A").is_some());
}

// https://github.com/foundry-rs/foundry/issues/5307
#[test]
fn can_create_standard_json_input_with_external_file() {
// File structure:
// .
// ├── verif
// │   └── src
// │   └── Counter.sol
// └── remapped
// ├── Child.sol
// └── Parent.sol

let dir = tempfile::tempdir().unwrap();
let verif_dir = utils::canonicalize(dir.path()).unwrap().join("verif");
let remapped_dir = utils::canonicalize(dir.path()).unwrap().join("remapped");
fs::create_dir_all(verif_dir.join("src")).unwrap();
fs::create_dir(&remapped_dir).unwrap();

let mut verif_project = Project::builder()
.paths(ProjectPathsConfig::dapptools(&verif_dir).unwrap())
.build()
.unwrap();

verif_project.paths.remappings.push(Remapping {
context: None,
name: "@remapped/".into(),
path: "../remapped/".into(),
});
verif_project.allowed_paths.insert(remapped_dir.clone());

fs::write(remapped_dir.join("Parent.sol"), "pragma solidity >=0.8.0; import './Child.sol';")
.unwrap();
fs::write(remapped_dir.join("Child.sol"), "pragma solidity >=0.8.0;").unwrap();
fs::write(
verif_dir.join("src/Counter.sol"),
"pragma solidity >=0.8.0; import '@remapped/Parent.sol'; contract Counter {}",
)
.unwrap();

// solc compiles using the host file system; therefore, this setup is considered valid
let compiled = verif_project.compile().unwrap();
compiled.assert_success();

// can create project root based paths
let std_json = verif_project.standard_json_input(verif_dir.join("src/Counter.sol")).unwrap();
assert_eq!(
std_json.sources.iter().map(|(path, _)| path.clone()).collect::<Vec<_>>(),
vec![
PathBuf::from("src/Counter.sol"),
PathBuf::from("../remapped/Parent.sol"),
PathBuf::from("../remapped/Child.sol")
]
);

// can compile using the created json
let compiler_errors = Solc::default()
.compile(&std_json)
.unwrap()
.errors
.into_iter()
.filter_map(|e| if e.severity.is_error() { Some(e.message) } else { None })
.collect::<Vec<_>>();
assert!(compiler_errors.is_empty(), "{:?}", compiler_errors);
}

#[test]
fn can_compile_std_json_input() {
let tmp = TempProject::dapptools_init().unwrap();
Expand Down

0 comments on commit 247161c

Please sign in to comment.