From a978bc98af2884db2ad9d62cc205f228b25134a8 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Thu, 28 Apr 2022 14:18:24 +0200 Subject: [PATCH] fix(solc): remapping aware libraries (#1190) * feat(solc): add Libraries type * feat: add lib applied remappings * test: add lib linking tests * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos * Update ethers-solc/src/artifacts/mod.rs Co-authored-by: Georgios Konstantopoulos * chore: rustfmt Co-authored-by: Georgios Konstantopoulos --- ethers-solc/src/artifacts/mod.rs | 182 +++++++++++++++++- ethers-solc/src/compile/mod.rs | 14 +- .../test-data/library-remapping-in.json | 39 ++++ ethers-solc/tests/project.rs | 131 ++++++++++++- 4 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 ethers-solc/test-data/library-remapping-in.json diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs index c34d221e..cd5e0f40 100644 --- a/ethers-solc/src/artifacts/mod.rs +++ b/ethers-solc/src/artifacts/mod.rs @@ -11,9 +11,12 @@ use std::{ str::FromStr, }; -use crate::{compile::*, error::SolcIoError, remappings::Remapping, utils}; +use crate::{ + compile::*, error::SolcIoError, remappings::Remapping, utils, ProjectPathsConfig, SolcError, +}; use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +use tracing::warn; pub mod ast; pub use ast::*; @@ -259,8 +262,15 @@ pub struct Settings { pub via_ir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub debug: Option, - #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] - pub libraries: BTreeMap>, + /// Addresses of the libraries. If not all libraries are given here, + /// it can result in unlinked objects whose output data is different. + /// + /// The top level key is the name of the source file where the library is used. + /// If remappings are used, this source file should match the global path + /// after remappings were applied. + /// If this key is an empty string, that refers to a global level. + #[serde(default, skip_serializing_if = "Libraries::is_empty")] + pub libraries: Libraries, } impl Settings { @@ -381,6 +391,101 @@ impl Default for Settings { } } +/// A wrapper type for all libraries in the form of `::` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct Libraries { + /// All libraries, `(file path -> (Lib name -> Address)) + pub libs: BTreeMap>, +} + +// === impl Libraries === + +impl Libraries { + /// Parses all libraries in the form of + /// `::` + /// + /// # Example + /// + /// ``` + /// use ethers_solc::artifacts::Libraries; + /// let libs = Libraries::parse(&[ + /// "src/DssSpell.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string(), + /// ]) + /// .unwrap(); + /// ``` + pub fn parse(libs: &[String]) -> Result { + let mut libraries = BTreeMap::default(); + for lib in libs { + let mut items = lib.split(':'); + let file = items.next().ok_or_else(|| { + SolcError::msg(format!("failed to parse path to library file: {}", lib)) + })?; + let lib = items + .next() + .ok_or_else(|| SolcError::msg(format!("failed to parse library name: {}", lib)))?; + let addr = items.next().ok_or_else(|| { + SolcError::msg(format!("failed to parse library address: {}", lib)) + })?; + if items.next().is_some() { + return Err(SolcError::msg(format!( + "failed to parse, too many arguments passed: {}", + lib + ))) + } + libraries + .entry(file.into()) + .or_insert_with(BTreeMap::default) + .insert(lib.to_string(), addr.to_string()); + } + Ok(Self { libs: libraries }) + } + + pub fn is_empty(&self) -> bool { + self.libs.is_empty() + } + + pub fn len(&self) -> usize { + self.libs.len() + } + + /// Solc expects the lib paths to match the global path after remappings were applied + /// + /// See also [ProjectPathsConfig::resolve_import] + pub fn with_applied_remappings(mut self, config: &ProjectPathsConfig) -> Self { + self.libs = self + .libs + .into_iter() + .map(|(file, target)| { + let file = config.resolve_import(&config.root, &file).unwrap_or_else(|err| { + warn!(target: "libs", "Failed to resolve library `{}` for linking: {:?}", file.display(), err); + file + }); + (file, target) + }) + .collect(); + self + } +} + +impl From>> for Libraries { + fn from(libs: BTreeMap>) -> Self { + Self { libs } + } +} + +impl AsRef>> for Libraries { + fn as_ref(&self) -> &BTreeMap> { + &self.libs + } +} + +impl AsMut>> for Libraries { + fn as_mut(&mut self) -> &mut BTreeMap> { + &mut self.libs + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Optimizer { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -772,13 +877,13 @@ pub struct Doc { #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub methods: Option, + pub methods: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct Libraries { +pub struct DocLibraries { #[serde(flatten)] pub libs: BTreeMap, } @@ -1631,4 +1736,71 @@ mod tests { let i = input.sanitized(&version); assert!(i.settings.metadata.unwrap().bytecode_hash.is_none()); } + + #[test] + fn can_parse_libraries() { + let libraries = ["./src/lib/LibraryContract.sol:Library:0xaddress".to_string()]; + + let libs = Libraries::parse(&libraries[..]).unwrap().libs; + + assert_eq!( + libs, + BTreeMap::from([( + PathBuf::from("./src/lib/LibraryContract.sol"), + BTreeMap::from([("Library".to_string(), "0xaddress".to_string())]) + )]) + ); + } + + #[test] + fn can_parse_many_libraries() { + let libraries= [ + "./src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string(), + "./src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string(), + "./src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string(), + "./src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string(), + "./src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string(), + ]; + + let libs = Libraries::parse(&libraries[..]).unwrap().libs; + + pretty_assertions::assert_eq!( + libs, + BTreeMap::from([ + ( + PathBuf::from("./src/SizeAuctionDiscount.sol"), + BTreeMap::from([ + ( + "Chainlink".to_string(), + "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string() + ), + ( + "Math".to_string(), + "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string() + ) + ]) + ), + ( + PathBuf::from("./src/SizeAuction.sol"), + BTreeMap::from([ + ( + "ChainlinkTWAP".to_string(), + "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string() + ), + ( + "Math".to_string(), + "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string() + ) + ]) + ), + ( + PathBuf::from("./src/test/ChainlinkTWAP.t.sol"), + BTreeMap::from([( + "ChainlinkTWAP".to_string(), + "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string() + )]) + ), + ]) + ); + } } diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs index 57665002..0b5c5bed 100644 --- a/ethers-solc/src/compile/mod.rs +++ b/ethers-solc/src/compile/mod.rs @@ -650,7 +650,7 @@ impl> From for Solc { #[cfg(test)] mod tests { use super::*; - use crate::CompilerInput; + use crate::{Artifact, CompilerInput}; fn solc() -> Solc { Solc::default() @@ -692,6 +692,18 @@ mod tests { } } + #[test] + fn can_compile_with_remapped_links() { + let input: CompilerInput = + serde_json::from_str(include_str!("../../test-data/library-remapping-in.json")) + .unwrap(); + let out = solc().compile(&input).unwrap(); + let (_, mut contracts) = out.split(); + let contract = contracts.remove("LinkTest").unwrap(); + let bytecode = &contract.get_bytecode().unwrap().object; + assert!(!bytecode.is_unlinked()); + } + #[cfg(feature = "async")] #[tokio::test] async fn async_solc_compile_works() { diff --git a/ethers-solc/test-data/library-remapping-in.json b/ethers-solc/test-data/library-remapping-in.json new file mode 100644 index 00000000..63163d7f --- /dev/null +++ b/ethers-solc/test-data/library-remapping-in.json @@ -0,0 +1,39 @@ +{ + "language": "Solidity", + "sources": { + "/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/lib/remapping/MyLib.sol": { + "content": "\n// SPDX-License-Identifier: MIT\nlibrary MyLib {\n function foobar(uint256 a) public view returns (uint256) {\n \treturn a * 100;\n }\n}\n" + }, + "/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/src/LinkTest.sol": { + "content": "\n// SPDX-License-Identifier: MIT\nimport \"remapping/MyLib.sol\";\ncontract LinkTest {\n function foo() public returns (uint256) {\n return MyLib.foobar(1);\n }\n}\n" + } + }, + "settings": { + "remappings": [ + "remapping/=/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/lib/remapping/" + ], + "optimizer": { + "enabled": false, + "runs": 200 + }, + "outputSelection": { + "*": { + "": [ + "ast" + ], + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers" + ] + } + }, + "evmVersion": "london", + "libraries": { + "/private/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/tmp_dappPyXsdD/lib/remapping/MyLib.sol": { + "MyLib": "0x0000000000000000000000000000000000000000" + } + } + } +} \ No newline at end of file diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index 2957c8cc..e10674c0 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -1,14 +1,15 @@ //! project tests use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, io, path::{Path, PathBuf}, str::FromStr, }; +use ethers_core::types::Address; use ethers_solc::{ - artifacts::BytecodeHash, + artifacts::{BytecodeHash, Libraries}, cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME}, project_util::*, remappings::Remapping, @@ -155,7 +156,7 @@ fn can_compile_dapp_detect_changes_in_libs() { project .paths_mut() .remappings - .push(Remapping::from_str(&format!("remapping={}/", remapping.display())).unwrap()); + .push(Remapping::from_str(&format!("remapping/={}/", remapping.display())).unwrap()); let src = project .add_source( @@ -814,6 +815,130 @@ contract LinkTest { assert_eq!(bytecode.clone(), serde_json::from_str(&s).unwrap()); } +#[test] +fn can_apply_libraries() { + let mut tmp = TempProject::dapptools().unwrap(); + + tmp.add_source( + "LinkTest", + r#" +// SPDX-License-Identifier: MIT +import "./MyLib.sol"; +contract LinkTest { + function foo() public returns (uint256) { + return MyLib.foobar(1); + } +} +"#, + ) + .unwrap(); + + let lib = tmp + .add_source( + "MyLib", + r#" +// SPDX-License-Identifier: MIT +library MyLib { + function foobar(uint256 a) public view returns (uint256) { + return a * 100; + } +} +"#, + ) + .unwrap(); + + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + + assert!(compiled.find("MyLib").is_some()); + let contract = compiled.find("LinkTest").unwrap(); + let bytecode = &contract.bytecode.as_ref().unwrap().object; + assert!(bytecode.is_unlinked()); + + // provide the library settings to let solc link + tmp.project_mut().solc_config.settings.libraries = BTreeMap::from([( + lib, + BTreeMap::from([("MyLib".to_string(), format!("{:?}", Address::zero()))]), + )]) + .into(); + + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + + assert!(compiled.find("MyLib").is_some()); + let contract = compiled.find("LinkTest").unwrap(); + let bytecode = &contract.bytecode.as_ref().unwrap().object; + assert!(!bytecode.is_unlinked()); + + let libs = Libraries::parse(&[format!("./src/MyLib.sol:MyLib:{:?}", Address::zero())]).unwrap(); + // provide the library settings to let solc link + tmp.project_mut().solc_config.settings.libraries = libs.with_applied_remappings(tmp.paths()); + + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + + assert!(compiled.find("MyLib").is_some()); + let contract = compiled.find("LinkTest").unwrap(); + let bytecode = &contract.bytecode.as_ref().unwrap().object; + assert!(!bytecode.is_unlinked()); +} + +#[test] +fn can_apply_libraries_with_remappings() { + let mut tmp = TempProject::dapptools().unwrap(); + + let remapping = tmp.paths().libraries[0].join("remapping"); + tmp.paths_mut() + .remappings + .push(Remapping::from_str(&format!("remapping/={}/", remapping.display())).unwrap()); + + tmp.add_source( + "LinkTest", + r#" +// SPDX-License-Identifier: MIT +import "remapping/MyLib.sol"; +contract LinkTest { + function foo() public returns (uint256) { + return MyLib.foobar(1); + } +} +"#, + ) + .unwrap(); + + tmp.add_lib( + "remapping/MyLib", + r#" +// SPDX-License-Identifier: MIT +library MyLib { + function foobar(uint256 a) public view returns (uint256) { + return a * 100; + } +} +"#, + ) + .unwrap(); + + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + + assert!(compiled.find("MyLib").is_some()); + let contract = compiled.find("LinkTest").unwrap(); + let bytecode = &contract.bytecode.as_ref().unwrap().object; + assert!(bytecode.is_unlinked()); + + let libs = + Libraries::parse(&[format!("remapping/MyLib.sol:MyLib:{:?}", Address::zero())]).unwrap(); // provide the library settings to let solc link + tmp.project_mut().solc_config.settings.libraries = libs.with_applied_remappings(tmp.paths()); + + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + + assert!(compiled.find("MyLib").is_some()); + let contract = compiled.find("LinkTest").unwrap(); + let bytecode = &contract.bytecode.as_ref().unwrap().object; + assert!(!bytecode.is_unlinked()); +} #[test] fn can_recompile_with_changes() { let mut tmp = TempProject::dapptools().unwrap();