Repository: gakonst/ethers-rs
Pull request #1190 (2022-04-28 10:14:58)

fix(solc): remapping aware libraries
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
The solc docs say about libraries

```json
"libraries": {
      // The top level key is the 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.
      "myFile.sol": {
        "MyLib": "0x123123..."
      }
    },
```

currently we're not able to apply remappings, this adds support for this

Ref https://github.com/foundry-rs/foundry/issues/1338

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution
* add `Libraries` wrapper type with support for applying remappings
* add more tests
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog



Patch:
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<bool>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub debug: Option<DebuggingSettings>,
-    #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
-    pub libraries: BTreeMap<String, BTreeMap<String, String>>,
+    /// 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 `<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)]
 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<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub methods: Option<Libraries>,
+    pub methods: Option<DocLibraries>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub version: Option<u32>,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
-pub struct Libraries {
+pub struct DocLibraries {
     #[serde(flatten)]
     pub libs: BTreeMap<String, serde_json::Value>,
 }
@@ -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<T: Into<PathBuf>> From<T> 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() {


Test patch:
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();