Repository: gakonst/ethers-rs
Pull request #553 (2021-11-03 08:40:14)

feat(solc): allow providing --allow-args
Allows providing the `--allow-paths` parameter to Solc. It defaults to `root` if none is found.


Patch:
diff --git a/ethers-core/src/lib.rs b/ethers-core/src/lib.rs
index 4f1f8612..a0d3de26 100644
--- a/ethers-core/src/lib.rs
+++ b/ethers-core/src/lib.rs
@@ -51,6 +51,7 @@ pub mod abi;
 /// Various utilities
 pub mod utils;
 
+#[cfg(feature = "macros")]
 pub mod macros;
 
 // re-export rand to avoid potential confusion when there's rand version mismatches
diff --git a/ethers-core/src/macros/mod.rs b/ethers-core/src/macros/mod.rs
index dae67eba..6a857b11 100644
--- a/ethers-core/src/macros/mod.rs
+++ b/ethers-core/src/macros/mod.rs
@@ -1,4 +1,2 @@
 mod ethers_crate;
-
-#[cfg(feature = "macros")]
 pub use ethers_crate::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate};
diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs
index 9d4d7a1e..44edc409 100644
--- a/ethers-solc/src/compile.rs
+++ b/ethers-solc/src/compile.rs
@@ -63,7 +63,12 @@ pub static RELEASES: Lazy<Vec<Version>> = Lazy::new(|| {
 ///
 /// Supports sync and async functions.
 #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
-pub struct Solc(pub PathBuf);
+pub struct Solc {
+    /// Path to the `solc` executable
+    pub solc: PathBuf,
+    /// Additional arguments passed to the `solc` exectuable
+    pub args: Vec<String>,
+}
 
 impl Default for Solc {
     fn default() -> Self {
@@ -74,7 +79,25 @@ impl Default for Solc {
 impl Solc {
     /// A new instance which points to `solc`
     pub fn new(path: impl Into<PathBuf>) -> Self {
-        Solc(path.into())
+        Solc { solc: path.into(), args: Vec::new() }
+    }
+
+    /// Adds an argument to pass to the `solc` command.
+    pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
+        self.args.push(arg.into());
+        self
+    }
+
+    /// Adds multiple arguments to pass to the `solc`.
+    pub fn args<I, S>(mut self, args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: Into<String>,
+    {
+        for arg in args {
+            self = self.arg(arg);
+        }
+        self
     }
 
     /// Returns the directory in which [svm](https://github.com/roynalnaruto/svm-rs) stores all versions
@@ -112,7 +135,7 @@ impl Solc {
         Ok(solc)
     }
 
-    /// Assuming the `versions` array is sorted, it returns the latest element which satisfies
+    /// Assuming the `versions` array is sorted, it returns the first element which satisfies
     /// the provided [`VersionReq`]
     pub fn find_matching_installation(
         versions: &[Version],
@@ -233,7 +256,10 @@ impl Solc {
     }
 
     pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
-        let mut child = Command::new(&self.0)
+        let mut cmd = Command::new(&self.solc);
+
+        let mut child = cmd
+            .args(&self.args)
             .arg("--standard-json")
             .stdin(Stdio::piped())
             .stderr(Stdio::piped())
@@ -248,7 +274,7 @@ impl Solc {
     /// Returns the version from the configured `solc`
     pub fn version(&self) -> Result<Version> {
         version_from_output(
-            Command::new(&self.0)
+            Command::new(&self.solc)
                 .arg("--version")
                 .stdin(Stdio::piped())
                 .stderr(Stdio::piped())
@@ -288,7 +314,7 @@ impl Solc {
     pub async fn async_compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
         use tokio::io::AsyncWriteExt;
         let content = serde_json::to_vec(input)?;
-        let mut child = tokio::process::Command::new(&self.0)
+        let mut child = tokio::process::Command::new(&self.solc)
             .arg("--standard-json")
             .stdin(Stdio::piped())
             .stderr(Stdio::piped())
@@ -302,7 +328,7 @@ impl Solc {
 
     pub async fn async_version(&self) -> Result<Version> {
         version_from_output(
-            tokio::process::Command::new(&self.0)
+            tokio::process::Command::new(&self.solc)
                 .arg("--version")
                 .stdin(Stdio::piped())
                 .stderr(Stdio::piped())
@@ -338,13 +364,13 @@ fn version_from_output(output: Output) -> Result<Version> {
 
 impl AsRef<Path> for Solc {
     fn as_ref(&self) -> &Path {
-        &self.0
+        &self.solc
     }
 }
 
 impl<T: Into<PathBuf>> From<T> for Solc {
     fn from(solc: T) -> Self {
-        Solc(solc.into())
+        Solc::new(solc.into())
     }
 }
 
@@ -451,7 +477,7 @@ mod tests {
         }
         let res = Solc::find_svm_installed_version(&version.to_string()).unwrap().unwrap();
         let expected = svm::SVM_HOME.join(ver).join(format!("solc-{}", ver));
-        assert_eq!(res.0, expected);
+        assert_eq!(res.solc, expected);
     }
 
     #[test]
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 744c9678..d6a22c03 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -272,3 +272,46 @@ impl fmt::Debug for ArtifactOutput {
         }
     }
 }
+
+use std::convert::TryFrom;
+
+/// Helper struct for serializing `--allow-paths` arguments to Solc
+///
+/// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping):
+/// For security reasons the compiler has restrictions on what directories it can access.
+/// Directories of source files specified on the command line and target paths of
+/// remappings are automatically allowed to be accessed by the file reader,
+/// but everything else is rejected by default. Additional paths (and their subdirectories)
+/// can be allowed via the --allow-paths /sample/path,/another/sample/path switch.
+/// Everything inside the path specified via --base-path is always allowed.
+#[derive(Clone, Debug, Default)]
+pub struct AllowedLibPaths(pub(crate) Vec<PathBuf>);
+
+impl fmt::Display for AllowedLibPaths {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let lib_paths = self
+            .0
+            .iter()
+            .filter(|path| path.exists())
+            .map(|path| format!("{}", path.display()))
+            .collect::<Vec<_>>()
+            .join(",");
+        write!(f, "{}", lib_paths)
+    }
+}
+
+impl<T: Into<PathBuf>> TryFrom<Vec<T>> for AllowedLibPaths {
+    type Error = std::io::Error;
+
+    fn try_from(libs: Vec<T>) -> std::result::Result<Self, Self::Error> {
+        let libs = libs
+            .into_iter()
+            .map(|lib| {
+                let path: PathBuf = lib.into();
+                let lib = std::fs::canonicalize(path)?;
+                Ok(lib)
+            })
+            .collect::<std::result::Result<Vec<_>, std::io::Error>>()?;
+        Ok(AllowedLibPaths(libs))
+    }
+}
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index eb5d03f8..0446d107 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -11,7 +11,7 @@ mod compile;
 pub use compile::*;
 
 mod config;
-pub use config::{ArtifactOutput, ProjectPathsConfig, SolcConfig};
+pub use config::{AllowedLibPaths, ArtifactOutput, ProjectPathsConfig, SolcConfig};
 
 use crate::{artifacts::Source, cache::SolFilesCache};
 
@@ -21,6 +21,7 @@ use crate::artifacts::Sources;
 use error::Result;
 use std::{
     collections::{BTreeMap, HashMap},
+    convert::TryInto,
     fmt, fs, io,
     path::PathBuf,
 };
@@ -40,6 +41,8 @@ pub struct Project {
     pub artifacts: ArtifactOutput,
     /// Errors/Warnings which match these error codes are not going to be logged
     pub ignored_error_codes: Vec<u64>,
+    /// The paths which will be allowed for library inclusion
+    pub allowed_lib_paths: AllowedLibPaths,
 }
 
 impl Project {
@@ -116,8 +119,12 @@ impl Project {
             let version = Solc::detect_version(&source)?;
             // gets the solc binary for that version, it is expected tha this will succeed
             // AND find the solc since it was installed right above
-            let solc = Solc::find_svm_installed_version(version.to_string())?
+            let mut solc = Solc::find_svm_installed_version(version.to_string())?
                 .expect("solc should have been installed");
+
+            if !self.allowed_lib_paths.0.is_empty() {
+                solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
+            }
             let entry = sources_by_version.entry(solc).or_insert_with(BTreeMap::new);
             entry.insert(path, source);
         }
@@ -132,7 +139,6 @@ impl Project {
                 res.contracts.extend(compiled.contracts);
             }
         }
-
         Ok(if res.contracts.is_empty() {
             ProjectCompileOutput::Unchanged
         } else {
@@ -214,6 +220,8 @@ pub struct ProjectBuilder {
     artifacts: Option<ArtifactOutput>,
     /// Which error codes to ignore
     pub ignored_error_codes: Vec<u64>,
+    /// All allowed paths
+    pub allowed_paths: Vec<PathBuf>,
 }
 
 impl ProjectBuilder {
@@ -248,8 +256,34 @@ impl ProjectBuilder {
         self
     }
 
+    /// Adds an allowed-path to the solc executable
+    pub fn allowed_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
+        self.allowed_paths.push(path.into());
+        self
+    }
+
+    /// Adds multiple allowed-path to the solc executable
+    pub fn allowed_paths<I, S>(mut self, args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: Into<PathBuf>,
+    {
+        for arg in args {
+            self = self.allowed_path(arg);
+        }
+        self
+    }
+
     pub fn build(self) -> Result<Project> {
-        let Self { paths, solc, solc_config, cached, artifacts, ignored_error_codes } = self;
+        let Self {
+            paths,
+            solc,
+            solc_config,
+            cached,
+            artifacts,
+            ignored_error_codes,
+            mut allowed_paths,
+        } = self;
 
         let solc = solc.unwrap_or_default();
         let solc_config = solc_config.map(Ok).unwrap_or_else(|| {
@@ -257,13 +291,21 @@ impl ProjectBuilder {
             SolcConfig::builder().version(version.to_string()).build()
         })?;
 
+        let paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
+
+        if allowed_paths.is_empty() {
+            // allow every contract under root by default
+            allowed_paths.push(paths.root.clone())
+        }
+
         Ok(Project {
-            paths: paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?,
+            paths,
             solc,
             solc_config,
             cached,
             artifacts: artifacts.unwrap_or_default(),
             ignored_error_codes,
+            allowed_lib_paths: allowed_paths.try_into()?,
         })
     }
 }
@@ -277,6 +319,7 @@ impl Default for ProjectBuilder {
             cached: true,
             artifacts: None,
             ignored_error_codes: Vec::new(),
+            allowed_paths: vec![],
         }
     }
 }
@@ -329,4 +372,35 @@ mod tests {
         // Contracts A to F
         assert_eq!(contracts.keys().count(), 5);
     }
+
+    #[test]
+    #[cfg(all(feature = "svm", feature = "async"))]
+    fn test_build_many_libs() {
+        use super::*;
+
+        let root = std::fs::canonicalize("./test-data/test-contract-libs").unwrap();
+
+        let paths = ProjectPathsConfig::builder()
+            .root(&root)
+            .sources(root.join("src"))
+            .lib(root.join("lib1"))
+            .lib(root.join("lib2"))
+            .build()
+            .unwrap();
+        let project = Project::builder()
+            .paths(paths)
+            .ephemeral()
+            .artifacts(ArtifactOutput::Nothing)
+            .build()
+            .unwrap();
+        let compiled = project.compile().unwrap();
+        let contracts = match compiled {
+            ProjectCompileOutput::Compiled((out, _)) => {
+                assert!(!out.has_error());
+                out.contracts
+            }
+            _ => panic!("must compile"),
+        };
+        assert_eq!(contracts.keys().count(), 3);
+    }
 }


Test patch:
diff --git a/ethers-solc/test-data/test-contract-libs/lib1/Bar.sol b/ethers-solc/test-data/test-contract-libs/lib1/Bar.sol
new file mode 100644
index 00000000..16dfcecb
--- /dev/null
+++ b/ethers-solc/test-data/test-contract-libs/lib1/Bar.sol
@@ -0,0 +1,3 @@
+pragma solidity ^0.8.6;
+
+contract Bar {}
diff --git a/ethers-solc/test-data/test-contract-libs/lib2/Baz.sol b/ethers-solc/test-data/test-contract-libs/lib2/Baz.sol
new file mode 100644
index 00000000..e4527d07
--- /dev/null
+++ b/ethers-solc/test-data/test-contract-libs/lib2/Baz.sol
@@ -0,0 +1,3 @@
+pragma solidity ^0.8.6;
+
+contract Baz {}
diff --git a/ethers-solc/test-data/test-contract-libs/src/Foo.sol b/ethers-solc/test-data/test-contract-libs/src/Foo.sol
new file mode 100644
index 00000000..ab9eba94
--- /dev/null
+++ b/ethers-solc/test-data/test-contract-libs/src/Foo.sol
@@ -0,0 +1,6 @@
+pragma solidity 0.8.6;
+
+import "../lib1/Bar.sol";
+import "../lib2/Baz.sol";
+
+contract Foo is Bar, Baz {}