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 <me@gakonst.com>

* Update ethers-solc/src/artifacts/mod.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* Update ethers-solc/src/artifacts/mod.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* Update ethers-solc/src/artifacts/mod.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* chore: rustfmt

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Matthias Seitz 2022-04-28 14:18:24 +02:00 committed by GitHub
parent 19f7a93243
commit a978bc98af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 357 additions and 9 deletions

View File

@ -11,9 +11,12 @@ use std::{
str::FromStr, 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 serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use tracing::warn;
pub mod ast; pub mod ast;
pub use ast::*; pub use ast::*;
@ -259,8 +262,15 @@ pub struct Settings {
pub via_ir: Option<bool>, pub via_ir: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub debug: Option<DebuggingSettings>, pub debug: Option<DebuggingSettings>,
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] /// Addresses of the libraries. If not all libraries are given here,
pub libraries: BTreeMap<String, BTreeMap<String, String>>, /// 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 { impl Settings {
@ -381,6 +391,101 @@ impl Default for Settings {
} }
} }
/// A wrapper type for all libraries in the form of `<file>:<lib>:<addr>`
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct Libraries {
/// All libraries, `(file path -> (Lib name -> Address))
pub libs: BTreeMap<PathBuf, BTreeMap<String, String>>,
}
// === impl Libraries ===
impl Libraries {
/// Parses all libraries in the form of
/// `<file>:<lib>:<addr>`
///
/// # Example
///
/// ```
/// use ethers_solc::artifacts::Libraries;
/// let libs = Libraries::parse(&[
/// "src/DssSpell.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string(),
/// ])
/// .unwrap();
/// ```
pub fn parse(libs: &[String]) -> Result<Self, SolcError> {
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<BTreeMap<PathBuf, BTreeMap<String, String>>> for Libraries {
fn from(libs: BTreeMap<PathBuf, BTreeMap<String, String>>) -> Self {
Self { libs }
}
}
impl AsRef<BTreeMap<PathBuf, BTreeMap<String, String>>> for Libraries {
fn as_ref(&self) -> &BTreeMap<PathBuf, BTreeMap<String, String>> {
&self.libs
}
}
impl AsMut<BTreeMap<PathBuf, BTreeMap<String, String>>> for Libraries {
fn as_mut(&mut self) -> &mut BTreeMap<PathBuf, BTreeMap<String, String>> {
&mut self.libs
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Optimizer { pub struct Optimizer {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@ -772,13 +877,13 @@ pub struct Doc {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>, pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub methods: Option<Libraries>, pub methods: Option<DocLibraries>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u32>, pub version: Option<u32>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Libraries { pub struct DocLibraries {
#[serde(flatten)] #[serde(flatten)]
pub libs: BTreeMap<String, serde_json::Value>, pub libs: BTreeMap<String, serde_json::Value>,
} }
@ -1631,4 +1736,71 @@ mod tests {
let i = input.sanitized(&version); let i = input.sanitized(&version);
assert!(i.settings.metadata.unwrap().bytecode_hash.is_none()); 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()
)])
),
])
);
}
} }

View File

@ -650,7 +650,7 @@ impl<T: Into<PathBuf>> From<T> for Solc {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::CompilerInput; use crate::{Artifact, CompilerInput};
fn solc() -> Solc { fn solc() -> Solc {
Solc::default() 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")] #[cfg(feature = "async")]
#[tokio::test] #[tokio::test]
async fn async_solc_compile_works() { async fn async_solc_compile_works() {

View File

@ -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"
}
}
}
}

View File

@ -1,14 +1,15 @@
//! project tests //! project tests
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use ethers_core::types::Address;
use ethers_solc::{ use ethers_solc::{
artifacts::BytecodeHash, artifacts::{BytecodeHash, Libraries},
cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME}, cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME},
project_util::*, project_util::*,
remappings::Remapping, remappings::Remapping,
@ -155,7 +156,7 @@ fn can_compile_dapp_detect_changes_in_libs() {
project project
.paths_mut() .paths_mut()
.remappings .remappings
.push(Remapping::from_str(&format!("remapping={}/", remapping.display())).unwrap()); .push(Remapping::from_str(&format!("remapping/={}/", remapping.display())).unwrap());
let src = project let src = project
.add_source( .add_source(
@ -814,6 +815,130 @@ contract LinkTest {
assert_eq!(bytecode.clone(), serde_json::from_str(&s).unwrap()); 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] #[test]
fn can_recompile_with_changes() { fn can_recompile_with_changes() {
let mut tmp = TempProject::dapptools().unwrap(); let mut tmp = TempProject::dapptools().unwrap();