Repository: gakonst/ethers-rs
Pull request #1590 (2022-08-11 22:13:20)

feat(solc): resolve absolute imports in 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
Closes https://github.com/foundry-rs/foundry/issues/2706
Resolve absolute imports like `src/interfaces/File.sol` in nested libs.
<!--
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
try to find the existing file be scanning the `cwd` until we reach the `lib` folder, see docs
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


mattsse: ## Update

absolute import paths in nested libs require `--include-path`

This also adds `IncludePaths` support and enables it if `solc > 0.8.8` which introduced --include-path.

By default, includepaths will be src,test,script, this now makes forge `src` remappings obsolete.

when resolving libraries we also track additional dirs that should be added to the `IncludePaths` set
mattsse: foundry tests also pass https://github.com/foundry-rs/foundry/runs/7809997586?check_suite_focus=true

with https://github.com/foundry-rs/foundry/pull/2747

Patch:
diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs
index dde89f04..46ebed6c 100644
--- a/ethers-solc/src/compile/mod.rs
+++ b/ethers-solc/src/compile/mod.rs
@@ -40,6 +40,14 @@ pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
 /// <https://blog.soliditylang.org/2021/08/11/solidity-0.8.7-release-announcement/>
 pub const LONDON_SOLC: Version = Version::new(0, 8, 7);
 
+// `--base-path` was introduced in 0.6.9 <https://github.com/ethereum/solidity/releases/tag/v0.6.9>
+pub static SUPPORTS_BASE_PATH: once_cell::sync::Lazy<VersionReq> =
+    once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
+
+// `--include-path` was introduced in 0.8.8 <https://github.com/ethereum/solidity/releases/tag/v0.8.8>
+pub static SUPPORTS_INCLUDE_PATH: once_cell::sync::Lazy<VersionReq> =
+    once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
+
 #[cfg(any(test, feature = "tests"))]
 use std::sync::Mutex;
 
@@ -527,6 +535,7 @@ impl Solc {
         let mut cmd = Command::new(&self.solc);
         if let Some(ref base_path) = self.base_path {
             cmd.current_dir(base_path);
+            cmd.arg("--base-path").arg(base_path);
         }
         let mut child = cmd
             .args(&self.args)
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index 5275efbe..f0fdce64 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -156,8 +156,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
         let graph = Graph::resolve_sources(&project.paths, sources)?;
         let (versions, edges) = graph.into_sources_by_version(project.offline)?;
 
-        let base_path = project.root();
-        let sources_by_version = versions.get(&project.allowed_lib_paths, base_path)?;
+        let sources_by_version = versions.get(project)?;
 
         let sources = if project.solc_jobs > 1 && sources_by_version.len() > 1 {
             // if there are multiple different versions, and we can use multiple jobs we can compile
@@ -178,6 +177,14 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
     ) -> Result<Self> {
         let version = solc.version()?;
         let (sources, edges) = Graph::resolve_sources(&project.paths, sources)?.into_sources();
+
+        // make sure `solc` has all required arguments
+        let solc = project.configure_solc_with_version(
+            solc,
+            Some(version.clone()),
+            edges.include_paths().clone(),
+        );
+
         let sources_by_version = BTreeMap::from([(solc, (version, sources))]);
         let sources = CompilerSources::Sequential(sources_by_version);
 
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 30714b76..41a52ad3 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -1,19 +1,17 @@
 use crate::{
-    artifacts::Settings,
+    artifacts::{output_selection::ContractOutputSelection, Settings},
     cache::SOLIDITY_FILES_CACHE_FILENAME,
     error::{Result, SolcError, SolcIoError},
     remappings::Remapping,
     resolver::{Graph, SolImportAlias},
     utils, Source, Sources,
 };
-
-use crate::artifacts::output_selection::ContractOutputSelection;
-
 use serde::{Deserialize, Serialize};
 use std::{
     collections::{BTreeSet, HashSet},
     fmt::{self, Formatter},
     fs,
+    ops::{Deref, DerefMut},
     path::{Component, Path, PathBuf},
 };
 
@@ -86,6 +84,15 @@ impl ProjectPathsConfig {
         paths
     }
 
+    /// Returns all `--include-path` paths that should be used for this project
+    ///
+    /// See [IncludePaths]
+    pub fn include_paths(&self) -> Vec<PathBuf> {
+        // Note: root must not be included, since it will be used as base-path, which would be a
+        // conflict
+        vec![self.sources.clone(), self.tests.clone(), self.scripts.clone()]
+    }
+
     /// Creates all configured dirs and files
     pub fn create_all(&self) -> std::result::Result<(), SolcIoError> {
         if let Some(parent) = self.cache.parent() {
@@ -214,11 +221,20 @@ impl ProjectPathsConfig {
     /// Attempts to resolve an `import` from the given working directory.
     ///
     /// The `cwd` path is the parent dir of the file that includes the `import`
-    pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result<PathBuf> {
+    ///
+    /// This will also populate the `include_paths` with any nested library root paths that should
+    /// be provided to solc via `--include-path` because it uses absolute imports.
+    pub fn resolve_import_and_include_paths(
+        &self,
+        cwd: &Path,
+        import: &Path,
+        include_paths: &mut IncludePaths,
+    ) -> Result<PathBuf> {
         let component = import
             .components()
             .next()
             .ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?;
+
         if component == Component::CurDir || component == Component::ParentDir {
             // if the import is relative we assume it's already part of the processed input
             // file set
@@ -227,7 +243,32 @@ impl ProjectPathsConfig {
             })
         } else {
             // resolve library file
-            self.resolve_library_import(import.as_ref()).ok_or_else(|| {
+            let resolved = self.resolve_library_import(import.as_ref());
+
+            if resolved.is_none() {
+                // absolute paths in solidity are a thing for example `import
+                // "src/interfaces/IConfig.sol"` which could either point to `cwd +
+                // src/interfaces/IConfig.sol`, or make use of a remapping (`src/=....`)
+                if let Some(lib) = self.find_library_ancestor(cwd) {
+                    if let Some((include_path, import)) =
+                        utils::resolve_absolute_library(lib, cwd, import)
+                    {
+                        // track the path for this absolute import inside a nested library
+                        include_paths.insert(include_path);
+                        return Ok(import)
+                    }
+                }
+                // also try to resolve absolute imports from the project paths
+                for path in [&self.root, &self.sources, &self.tests, &self.scripts] {
+                    if cwd.starts_with(path) {
+                        if let Ok(import) = utils::canonicalize(path.join(import)) {
+                            return Ok(import)
+                        }
+                    }
+                }
+            }
+
+            resolved.ok_or_else(|| {
                 SolcError::msg(format!(
                     "failed to resolve library import \"{:?}\"",
                     import.display()
@@ -236,6 +277,13 @@ impl ProjectPathsConfig {
         }
     }
 
+    /// Attempts to resolve an `import` from the given working directory.
+    ///
+    /// The `cwd` path is the parent dir of the file that includes the `import`
+    pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result<PathBuf> {
+        self.resolve_import_and_include_paths(cwd, import, &mut Default::default())
+    }
+
     /// Attempts to find the path to the real solidity file that's imported via the given `import`
     /// path by applying the configured remappings and checking the library dirs
     ///
@@ -751,6 +799,49 @@ impl SolcConfigBuilder {
     }
 }
 
+/// Container for all `--include-path` arguments for Solc, se also [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping
+///
+/// The `--include--path` flag:
+/// > Makes an additional source directory available to the default import callback. Use this option
+/// > if you want to import contracts whose location is not fixed in relation to your main source
+/// > tree, e.g. third-party libraries installed using a package manager. Can be used multiple
+/// > times. Can only be used if base path has a non-empty value.
+///
+/// In contrast to `--allow-paths` [`AllowedLibPaths`], which takes multiple arguments,
+/// `--include-path` only takes a single path argument.
+#[derive(Clone, Debug, Default)]
+pub struct IncludePaths(pub(crate) BTreeSet<PathBuf>);
+
+// === impl IncludePaths ===
+
+impl IncludePaths {
+    /// Returns the [Command](std::process::Command) arguments for this type
+    ///
+    /// For each entry in the set, it will return `--include-path` + `<entry>`
+    pub fn args(&self) -> impl Iterator<Item = String> + '_ {
+        self.paths().flat_map(|path| ["--include-path".to_string(), format!("{}", path.display())])
+    }
+
+    /// Returns all paths that exist
+    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> + '_ {
+        self.0.iter().filter(|path| path.exists())
+    }
+}
+
+impl Deref for IncludePaths {
+    type Target = BTreeSet<PathBuf>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for IncludePaths {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
 /// 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):
@@ -761,23 +852,46 @@ impl SolcConfigBuilder {
 /// 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>);
+pub struct AllowedLibPaths(pub(crate) BTreeSet<PathBuf>);
+
+// === impl AllowedLibPaths ===
 
 impl AllowedLibPaths {
-    pub fn is_empty(&self) -> bool {
-        self.0.is_empty()
+    /// Returns the [Command](std::process::Command) arguments for this type
+    ///
+    /// `--allow-paths` takes a single value: all comma separated paths
+    pub fn args(&self) -> Option<[String; 2]> {
+        let args = self.to_string();
+        if args.is_empty() {
+            return None
+        }
+        Some(["--allow-paths".to_string(), args])
+    }
+
+    /// Returns all paths that exist
+    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> + '_ {
+        self.0.iter().filter(|path| path.exists())
+    }
+}
+
+impl Deref for AllowedLibPaths {
+    type Target = BTreeSet<PathBuf>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for AllowedLibPaths {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
     }
 }
 
 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(",");
+        let lib_paths =
+            self.paths().map(|path| format!("{}", path.display())).collect::<Vec<_>>().join(",");
         write!(f, "{}", lib_paths)
     }
 }
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 0e39c8a7..424c26c7 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -38,6 +38,7 @@ pub use filter::{FileFilter, TestFileFilter};
 use crate::{
     artifacts::Sources,
     cache::SolFilesCache,
+    config::IncludePaths,
     error::{SolcError, SolcIoError},
     sources::{VersionedSourceFile, VersionedSourceFiles},
 };
@@ -73,7 +74,9 @@ pub struct Project<T: ArtifactOutput = ConfigurableArtifacts> {
     /// 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,
+    pub allowed_paths: AllowedLibPaths,
+    /// The paths which will be used with solc's `--include-path` attribute
+    pub include_paths: IncludePaths,
     /// Maximum number of `solc` processes to run simultaneously.
     solc_jobs: usize,
     /// Offline mode, if set, network access (download solc) is disallowed
@@ -151,16 +154,48 @@ impl<T: ArtifactOutput> Project<T> {
         SolFilesCache::read_joined(&self.paths)
     }
 
+    /// Applies the configured arguments to the given `Solc`
+    ///
+    /// See [Self::configure_solc_with_version()]
+    pub(crate) fn configure_solc(&self, solc: Solc) -> Solc {
+        let version = solc.version().ok();
+        self.configure_solc_with_version(solc, version, Default::default())
+    }
+
     /// Applies the configured arguments to the given `Solc`
     ///
     /// This will set the `--allow-paths` to the paths configured for the `Project`, if any.
-    fn configure_solc(&self, mut solc: Solc) -> Solc {
-        if !self.allowed_lib_paths.0.is_empty() &&
-            !solc.args.iter().any(|arg| arg == "--allow-paths")
-        {
-            solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
+    ///
+    /// If a version is provided and it is applicable it will also set `--base-path` and
+    /// `--include-path` This will set the `--allow-paths` to the paths configured for the
+    /// `Project`, if any.
+    /// This also accepts additional `include_paths`
+    pub(crate) fn configure_solc_with_version(
+        &self,
+        mut solc: Solc,
+        version: Option<Version>,
+        mut include_paths: IncludePaths,
+    ) -> Solc {
+        if !solc.args.iter().any(|arg| arg == "--allow-paths") {
+            if let Some([allow, libs]) = self.allowed_paths.args() {
+                solc = solc.arg(allow).arg(libs);
+            }
         }
-        solc.with_base_path(self.root())
+        if let Some(version) = version {
+            if SUPPORTS_BASE_PATH.matches(&version) {
+                let base_path = format!("{}", self.root().display());
+                if !base_path.is_empty() {
+                    solc = solc.with_base_path(self.root());
+                    if SUPPORTS_INCLUDE_PATH.matches(&version) {
+                        include_paths.extend(self.include_paths.paths().cloned());
+                        solc = solc.args(include_paths.args());
+                    }
+                }
+            } else {
+                solc.base_path.take();
+            }
+        }
+        solc
     }
 
     /// Sets the maximum number of parallel `solc` processes to run simultaneously.
@@ -232,9 +267,7 @@ impl<T: ArtifactOutput> Project<T> {
             return self.svm_compile(sources)
         }
 
-        let solc = self.configure_solc(self.solc.clone());
-
-        self.compile_with_version(&solc, sources)
+        self.compile_with_version(&self.solc, sources)
     }
 
     /// Compiles a set of contracts using `svm` managed solc installs
@@ -360,13 +393,9 @@ impl<T: ArtifactOutput> Project<T> {
                 .compile()
         }
 
-        project::ProjectCompiler::with_sources_and_solc(
-            self,
-            sources,
-            self.configure_solc(self.solc.clone()),
-        )?
-        .with_sparse_output(filter)
-        .compile()
+        project::ProjectCompiler::with_sources_and_solc(self, sources, self.solc.clone())?
+            .with_sparse_output(filter)
+            .compile()
     }
 
     /// Compiles the given source files with the exact `Solc` executable
@@ -397,12 +426,7 @@ impl<T: ArtifactOutput> Project<T> {
         solc: &Solc,
         sources: Sources,
     ) -> Result<ProjectCompileOutput<T>> {
-        project::ProjectCompiler::with_sources_and_solc(
-            self,
-            sources,
-            self.configure_solc(solc.clone()),
-        )?
-        .compile()
+        project::ProjectCompiler::with_sources_and_solc(self, sources, solc.clone())?.compile()
     }
 
     /// Removes the project's artifacts and cache file
@@ -541,8 +565,10 @@ pub struct ProjectBuilder<T: ArtifactOutput = ConfigurableArtifacts> {
     artifacts: T,
     /// Which error codes to ignore
     pub ignored_error_codes: Vec<u64>,
-    /// All allowed paths
-    pub allowed_paths: Vec<PathBuf>,
+    /// All allowed paths for solc's `--allowed-paths`
+    allowed_paths: AllowedLibPaths,
+    /// Paths to use for solc's `--include-path`
+    include_paths: IncludePaths,
     solc_jobs: Option<usize>,
 }
 
@@ -561,7 +587,8 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             slash_paths: true,
             artifacts,
             ignored_error_codes: Vec::new(),
-            allowed_paths: vec![],
+            allowed_paths: Default::default(),
+            include_paths: Default::default(),
             solc_jobs: None,
         }
     }
@@ -697,6 +724,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             auto_detect,
             ignored_error_codes,
             allowed_paths,
+            include_paths,
             solc_jobs,
             offline,
             build_info,
@@ -715,6 +743,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             artifacts,
             ignored_error_codes,
             allowed_paths,
+            include_paths,
             solc_jobs,
             build_info,
         }
@@ -723,7 +752,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
     /// Adds an allowed-path to the solc executable
     #[must_use]
     pub fn allowed_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
-        self.allowed_paths.push(path.into());
+        self.allowed_paths.insert(path.into());
         self
     }
 
@@ -740,6 +769,26 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
         self
     }
 
+    /// Adds an `--include-path` to the solc executable
+    #[must_use]
+    pub fn include_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
+        self.include_paths.insert(path.into());
+        self
+    }
+
+    /// Adds multiple include-path to the solc executable
+    #[must_use]
+    pub fn include_paths<I, S>(mut self, args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: Into<PathBuf>,
+    {
+        for arg in args {
+            self = self.include_path(arg);
+        }
+        self
+    }
+
     pub fn build(self) -> Result<Project<T>> {
         let Self {
             paths,
@@ -751,6 +800,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             artifacts,
             ignored_error_codes,
             mut allowed_paths,
+            mut include_paths,
             solc_jobs,
             offline,
             build_info,
@@ -767,10 +817,10 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
         let solc = solc.unwrap_or_default();
         let solc_config = solc_config.unwrap_or_else(|| SolcConfig::builder().build());
 
-        if allowed_paths.is_empty() {
-            // allow every contract under root by default
-            allowed_paths.push(paths.root.clone())
-        }
+        // allow every contract under root by default
+        allowed_paths.insert(paths.root.clone());
+        // allow paths where contracts are stored by default
+        include_paths.extend(paths.include_paths());
 
         Ok(Project {
             paths,
@@ -782,8 +832,9 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             auto_detect,
             artifacts,
             ignored_error_codes,
-            allowed_lib_paths: allowed_paths.into(),
-            solc_jobs: solc_jobs.unwrap_or_else(::num_cpus::get),
+            allowed_paths,
+            include_paths,
+            solc_jobs: solc_jobs.unwrap_or_else(num_cpus::get),
             offline,
             slash_paths,
         })
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 374f678e..3c68f2d6 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -46,19 +46,16 @@
 //! [version pragma](https://docs.soliditylang.org/en/develop/layout-of-source-files.html#version-pragma),
 //! which is defined on a per source file basis.
 
+use crate::{error::Result, utils, IncludePaths, ProjectPathsConfig, SolcError, Source, Sources};
+use parse::{SolData, SolDataUnit, SolImport};
+use rayon::prelude::*;
+use semver::VersionReq;
 use std::{
     collections::{HashMap, HashSet, VecDeque},
     fmt, io,
     path::{Path, PathBuf},
 };
 
-use parse::{SolData, SolDataUnit, SolImport};
-use rayon::prelude::*;
-
-use semver::VersionReq;
-
-use crate::{error::Result, utils, ProjectPathsConfig, SolcError, Source, Sources};
-
 mod parse;
 mod tree;
 
@@ -90,6 +87,13 @@ pub struct GraphEdges {
     num_input_files: usize,
     /// tracks all imports that we failed to resolve for a file
     unresolved_imports: HashSet<(PathBuf, PathBuf)>,
+    /// tracks additional include paths resolved by scanning all imports of the graph
+    ///
+    /// Absolute imports, like `import "src/Contract.sol"` are possible, but this does not play
+    /// nice with the standard-json import format, since the VFS won't be able to resolve
+    /// "src/Contract.sol" without help via `--include-path`
+    #[allow(unused)]
+    resolved_solc_include_paths: IncludePaths,
 }
 
 impl GraphEdges {
@@ -113,6 +117,11 @@ impl GraphEdges {
         self.files().skip(self.num_input_files)
     }
 
+    /// Returns all additional `--include-paths`
+    pub fn include_paths(&self) -> &IncludePaths {
+        &self.resolved_solc_include_paths
+    }
+
     /// Returns all imports that we failed to resolve
     pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
         &self.unresolved_imports
@@ -333,6 +342,10 @@ impl Graph {
         let mut nodes = Vec::with_capacity(unresolved.len());
         let mut edges = Vec::with_capacity(unresolved.len());
 
+        // tracks additional paths that should be used with `--include-path`, these are libraries
+        // that use absolute imports like `import "src/Contract.sol"`
+        let mut resolved_solc_include_paths = IncludePaths::default();
+
         // keep track of all unique paths that we failed to resolve to not spam the reporter with
         // the same path
         let mut unresolved_imports = HashSet::new();
@@ -349,7 +362,11 @@ impl Graph {
 
             for import in node.data.imports.iter() {
                 let import_path = import.data().path();
-                match paths.resolve_import(cwd, import_path) {
+                match paths.resolve_import_and_include_paths(
+                    cwd,
+                    import_path,
+                    &mut resolved_solc_include_paths,
+                ) {
                     Ok(import) => {
                         add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
                     }
@@ -391,6 +408,7 @@ impl Graph {
                 .collect(),
             data: Default::default(),
             unresolved_imports,
+            resolved_solc_include_paths,
         };
         Ok(Graph { nodes, edges, root: paths.root.clone() })
     }
@@ -461,7 +479,14 @@ impl Graph {
             }
             versioned_sources.insert(version, sources);
         }
-        Ok((VersionedSources { inner: versioned_sources, offline }, edges))
+        Ok((
+            VersionedSources {
+                inner: versioned_sources,
+                offline,
+                resolved_solc_include_paths: edges.resolved_solc_include_paths.clone(),
+            },
+            edges,
+        ))
     }
 
     /// Writes the list of imported files into the given formatter:
@@ -713,6 +738,7 @@ impl<'a> Iterator for NodesIter<'a> {
 #[cfg(all(feature = "svm-solc"))]
 #[derive(Debug)]
 pub struct VersionedSources {
+    resolved_solc_include_paths: IncludePaths,
     inner: HashMap<crate::SolcVersion, Sources>,
     offline: bool,
 }
@@ -724,17 +750,11 @@ impl VersionedSources {
     /// This will also configure following solc arguments:
     ///    - `allowed_paths`
     ///    - `base_path`
-    pub fn get(
+    pub fn get<T: crate::ArtifactOutput>(
         self,
-        allowed_lib_paths: &crate::AllowedLibPaths,
-        base_path: impl AsRef<Path>,
+        project: &crate::Project<T>,
     ) -> Result<std::collections::BTreeMap<crate::Solc, (semver::Version, Sources)>> {
         use crate::Solc;
-
-        // `--base-path` was introduced in 0.6.9 <https://github.com/ethereum/solidity/releases/tag/v0.6.9>
-        static SUPPORTS_BASE_PATH: once_cell::sync::Lazy<VersionReq> =
-            once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
-
         // we take the installer lock here to ensure installation checking is done in sync
         #[cfg(any(test, feature = "tests"))]
         let _lock = crate::compile::take_solc_installer_lock();
@@ -771,16 +791,15 @@ impl VersionedSources {
                     tracing::trace!("reinstalled solc: \"{}\"", version);
                 }
             }
-            let mut solc = solc
-                .arg("--allow-paths")
-                .arg(allowed_lib_paths.to_string())
-                .with_base_path(base_path.as_ref());
-            let version = solc.version()?;
 
-            if SUPPORTS_BASE_PATH.matches(&version) {
-                solc = solc.arg("--base-path").arg(format!("{}", base_path.as_ref().display()));
-            }
+            let version = solc.version()?;
 
+            // this will configure the `Solc` executable and its arguments
+            let solc = project.configure_solc_with_version(
+                solc,
+                Some(version.clone()),
+                self.resolved_solc_include_paths.clone(),
+            );
             sources_by_version.insert(solc, (version, sources));
         }
         Ok(sources_by_version)
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index a14e6d27..89636366 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -223,6 +223,37 @@ pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> O
     }
 }
 
+/// Tries to find an absolute import like `src/interfaces/IConfig.sol` in `cwd`, moving up the path
+/// until the `root` is reached.
+///
+/// If an existing file under `root` is found, this returns the path up to the `import` path and the
+/// canonicalized `import` path itself:
+///
+/// For example for following layout:
+///
+/// ```text
+/// <root>/mydependency/
+/// ├── src (`cwd`)
+/// │   ├── interfaces
+/// │   │   ├── IConfig.sol
+/// ```
+/// and `import` as `src/interfaces/IConfig.sol` and `cwd` as `src` this will return
+/// (`<root>/mydependency/`, `<root>/mydependency/src/interfaces/IConfig.sol`)
+pub fn resolve_absolute_library(
+    root: &Path,
+    cwd: &Path,
+    import: &Path,
+) -> Option<(PathBuf, PathBuf)> {
+    let mut parent = cwd.parent()?;
+    while parent != root {
+        if let Ok(import) = canonicalize(parent.join(import)) {
+            return Some((parent.to_path_buf(), import))
+        }
+        parent = parent.parent()?;
+    }
+    None
+}
+
 /// Reads the list of Solc versions that have been installed in the machine. The version list is
 /// sorted in ascending order.
 /// Checks for installed solc versions under the given path as


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 7ce00970..4944e0ee 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -1202,7 +1202,7 @@ library MyLib {
 #[test]
 fn can_recompile_with_changes() {
     let mut tmp = TempProject::dapptools().unwrap();
-    tmp.project_mut().allowed_lib_paths = vec![tmp.root().join("modules")].into();
+    tmp.project_mut().allowed_paths = vec![tmp.root().join("modules")].into();
 
     let content = r#"
     pragma solidity ^0.8.10;
@@ -2211,3 +2211,110 @@ fn can_add_basic_contract_and_library() {
     assert!(compiled.find_first("Foo").is_some());
     assert!(compiled.find_first("Bar").is_some());
 }
+
+// <https://github.com/foundry-rs/foundry/issues/2706>
+#[test]
+fn can_handle_nested_absolute_imports() {
+    let mut project = TempProject::dapptools().unwrap();
+
+    let remapping = project.paths().libraries[0].join("myDepdendency");
+    project
+        .paths_mut()
+        .remappings
+        .push(Remapping::from_str(&format!("myDepdendency/={}/", remapping.display())).unwrap());
+
+    project
+        .add_lib(
+            "myDepdendency/src/interfaces/IConfig.sol",
+            r#"
+    pragma solidity ^0.8.10;
+
+    interface IConfig {}
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_lib(
+            "myDepdendency/src/Config.sol",
+            r#"
+    pragma solidity ^0.8.10;
+    import "src/interfaces/IConfig.sol";
+
+    contract Config {}
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "Greeter",
+            r#"
+    pragma solidity ^0.8.10;
+    import "myDepdendency/src/Config.sol";
+
+    contract Greeter {}
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(compiled.find_first("Greeter").is_some());
+    assert!(compiled.find_first("Config").is_some());
+    assert!(compiled.find_first("IConfig").is_some());
+}
+
+#[test]
+fn can_handle_nested_test_absolute_imports() {
+    let project = TempProject::dapptools().unwrap();
+
+    project
+        .add_source(
+            "Contract.sol",
+            r#"
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity =0.8.13;
+
+library Library {
+    function f(uint256 a, uint256 b) public pure returns (uint256) {
+        return a + b;
+    }
+}
+
+contract Contract {
+    uint256 c;
+
+    constructor() {
+        c = Library.f(1, 2);
+    }
+}
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_test(
+            "Contract.t.sol",
+            r#"
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity =0.8.13;
+
+import "src/Contract.sol";
+
+contract ContractTest {
+    function setUp() public {
+    }
+
+    function test() public {
+        new Contract();
+    }
+}
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(compiled.find_first("Contract").is_some());
+}