Skip to content

Commit 247161c

Browse files
authored
fix: create verifiable Standard JSON for projects with external files (#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
1 parent b1561d8 commit 247161c

File tree

2 files changed

+162
-11
lines changed

2 files changed

+162
-11
lines changed

src/lib.rs

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -490,8 +490,6 @@ impl<T: ArtifactOutput> Project<T> {
490490
&self,
491491
target: impl AsRef<Path>,
492492
) -> Result<StandardJsonCompilerInput> {
493-
use path_slash::PathExt;
494-
495493
let target = target.as_ref();
496494
tracing::trace!("Building standard-json-input for {:?}", target);
497495
let graph = Graph::resolve(&self.paths)?;
@@ -514,14 +512,7 @@ impl<T: ArtifactOutput> Project<T> {
514512
let root = self.root();
515513
let sources = sources
516514
.into_iter()
517-
.map(|(path, source)| {
518-
let path: PathBuf = if let Ok(stripped) = path.strip_prefix(root) {
519-
stripped.to_slash_lossy().into_owned().into()
520-
} else {
521-
path.to_slash_lossy().into_owned().into()
522-
};
523-
(path, source.clone())
524-
})
515+
.map(|(path, source)| (rebase_path(root, path), source.clone()))
525516
.collect();
526517

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

948+
// Rebases the given path to the base directory lexically.
949+
//
950+
// For instance, given the base `/home/user/project` and the path `/home/user/project/src/A.sol`,
951+
// this function returns `src/A.sol`.
952+
//
953+
// This function transforms a path into a form that is relative to the base directory. The returned
954+
// path starts either with a normal component (e.g., `src`) or a parent directory component (i.e.,
955+
// `..`). It also converts the path into a UTF-8 string and replaces all separators with forward
956+
// slashes (`/`), if they're not.
957+
//
958+
// The rebasing process can be conceptualized as follows:
959+
//
960+
// 1. Remove the leading components from the path that match those in the base.
961+
// 2. Prepend `..` components to the path, matching the number of remaining components in the base.
962+
//
963+
// # Examples
964+
//
965+
// `rebase_path("/home/user/project", "/home/user/project/src/A.sol")` returns `src/A.sol`. The
966+
// common part, `/home/user/project`, is removed from the path.
967+
//
968+
// `rebase_path("/home/user/project", "/home/user/A.sol")` returns `../A.sol`. First, the common
969+
// part, `/home/user`, is removed, leaving `A.sol`. Next, as `project` remains in the base, `..` is
970+
// prepended to the path.
971+
//
972+
// On Windows, paths like `a\b\c` are converted to `a/b/c`.
973+
//
974+
// For more examples, see the test.
975+
fn rebase_path(base: impl AsRef<Path>, path: impl AsRef<Path>) -> PathBuf {
976+
use path_slash::PathExt;
977+
978+
let mut base_components = base.as_ref().components();
979+
let mut path_components = path.as_ref().components();
980+
981+
let mut new_path = PathBuf::new();
982+
983+
while let Some(path_component) = path_components.next() {
984+
let base_component = base_components.next();
985+
986+
if Some(path_component) != base_component {
987+
if base_component.is_some() {
988+
new_path.extend(
989+
std::iter::repeat(std::path::Component::ParentDir)
990+
.take(base_components.count() + 1),
991+
);
992+
}
993+
994+
new_path.push(path_component);
995+
new_path.extend(path_components);
996+
997+
break;
998+
}
999+
}
1000+
1001+
new_path.to_slash_lossy().into_owned().into()
1002+
}
1003+
9571004
#[cfg(test)]
9581005
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
9591006
mod tests {
@@ -1014,4 +1061,43 @@ mod tests {
10141061
let contracts = project.compile().unwrap().succeeded().output().contracts;
10151062
assert_eq!(contracts.contracts().count(), 2);
10161063
}
1064+
1065+
#[test]
1066+
fn can_rebase_path() {
1067+
assert_eq!(rebase_path("a/b", "a/b/c"), PathBuf::from("c"));
1068+
assert_eq!(rebase_path("a/b", "a/c"), PathBuf::from("../c"));
1069+
assert_eq!(rebase_path("a/b", "c"), PathBuf::from("../../c"));
1070+
1071+
assert_eq!(
1072+
rebase_path("/home/user/project", "/home/user/project/A.sol"),
1073+
PathBuf::from("A.sol")
1074+
);
1075+
assert_eq!(
1076+
rebase_path("/home/user/project", "/home/user/project/src/A.sol"),
1077+
PathBuf::from("src/A.sol")
1078+
);
1079+
assert_eq!(
1080+
rebase_path("/home/user/project", "/home/user/project/lib/forge-std/src/Test.sol"),
1081+
PathBuf::from("lib/forge-std/src/Test.sol")
1082+
);
1083+
assert_eq!(
1084+
rebase_path("/home/user/project", "/home/user/A.sol"),
1085+
PathBuf::from("../A.sol")
1086+
);
1087+
assert_eq!(rebase_path("/home/user/project", "/home/A.sol"), PathBuf::from("../../A.sol"));
1088+
assert_eq!(rebase_path("/home/user/project", "/A.sol"), PathBuf::from("../../../A.sol"));
1089+
assert_eq!(
1090+
rebase_path("/home/user/project", "/tmp/A.sol"),
1091+
PathBuf::from("../../../tmp/A.sol")
1092+
);
1093+
1094+
assert_eq!(
1095+
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/remapped/Child.sol"),
1096+
PathBuf::from("../remapped/Child.sol")
1097+
);
1098+
assert_eq!(
1099+
rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/verif/../remapped/Parent.sol"),
1100+
PathBuf::from("../remapped/Parent.sol")
1101+
);
1102+
}
10171103
}

tests/project.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use foundry_compilers::{
1212
info::ContractInfo,
1313
project_util::*,
1414
remappings::Remapping,
15-
Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project,
15+
utils, Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project,
1616
ProjectCompileOutput, ProjectPathsConfig, Solc, TestFileFilter,
1717
};
1818
use pretty_assertions::assert_eq;
@@ -1600,6 +1600,71 @@ fn can_sanitize_bytecode_hash() {
16001600
assert!(compiled.find_first("A").is_some());
16011601
}
16021602

1603+
// https://github.com/foundry-rs/foundry/issues/5307
1604+
#[test]
1605+
fn can_create_standard_json_input_with_external_file() {
1606+
// File structure:
1607+
// .
1608+
// ├── verif
1609+
// │   └── src
1610+
// │   └── Counter.sol
1611+
// └── remapped
1612+
// ├── Child.sol
1613+
// └── Parent.sol
1614+
1615+
let dir = tempfile::tempdir().unwrap();
1616+
let verif_dir = utils::canonicalize(dir.path()).unwrap().join("verif");
1617+
let remapped_dir = utils::canonicalize(dir.path()).unwrap().join("remapped");
1618+
fs::create_dir_all(verif_dir.join("src")).unwrap();
1619+
fs::create_dir(&remapped_dir).unwrap();
1620+
1621+
let mut verif_project = Project::builder()
1622+
.paths(ProjectPathsConfig::dapptools(&verif_dir).unwrap())
1623+
.build()
1624+
.unwrap();
1625+
1626+
verif_project.paths.remappings.push(Remapping {
1627+
context: None,
1628+
name: "@remapped/".into(),
1629+
path: "../remapped/".into(),
1630+
});
1631+
verif_project.allowed_paths.insert(remapped_dir.clone());
1632+
1633+
fs::write(remapped_dir.join("Parent.sol"), "pragma solidity >=0.8.0; import './Child.sol';")
1634+
.unwrap();
1635+
fs::write(remapped_dir.join("Child.sol"), "pragma solidity >=0.8.0;").unwrap();
1636+
fs::write(
1637+
verif_dir.join("src/Counter.sol"),
1638+
"pragma solidity >=0.8.0; import '@remapped/Parent.sol'; contract Counter {}",
1639+
)
1640+
.unwrap();
1641+
1642+
// solc compiles using the host file system; therefore, this setup is considered valid
1643+
let compiled = verif_project.compile().unwrap();
1644+
compiled.assert_success();
1645+
1646+
// can create project root based paths
1647+
let std_json = verif_project.standard_json_input(verif_dir.join("src/Counter.sol")).unwrap();
1648+
assert_eq!(
1649+
std_json.sources.iter().map(|(path, _)| path.clone()).collect::<Vec<_>>(),
1650+
vec![
1651+
PathBuf::from("src/Counter.sol"),
1652+
PathBuf::from("../remapped/Parent.sol"),
1653+
PathBuf::from("../remapped/Child.sol")
1654+
]
1655+
);
1656+
1657+
// can compile using the created json
1658+
let compiler_errors = Solc::default()
1659+
.compile(&std_json)
1660+
.unwrap()
1661+
.errors
1662+
.into_iter()
1663+
.filter_map(|e| if e.severity.is_error() { Some(e.message) } else { None })
1664+
.collect::<Vec<_>>();
1665+
assert!(compiler_errors.is_empty(), "{:?}", compiler_errors);
1666+
}
1667+
16031668
#[test]
16041669
fn can_compile_std_json_input() {
16051670
let tmp = TempProject::dapptools_init().unwrap();

0 commit comments

Comments
 (0)