Repository: gakonst/ethers-rs
Pull request #1302 (2022-05-23 13:07:09)

feat(providers): add `RetryClient`
<!--
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

https://github.com/gakonst/ethers-rs/issues/1291

## Solution

Instead of relying on non-standard status codes and errors for rate limiting, maintain the current state of rate limits and enforce the limits on the client side such that the rate limit is never hit on the server side. This requires for the client to know the rate limit i.e. the `quota` that renews every `duration` amount of time. 

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


mattsse: > This requires for the client to know the rate limit i.e. the quota that renews every duration amount of time.

this info is not always available and will also result in errors if the server simply decides we're getting rate limited even if we follow the limits.
adding support for retries should be more convenient
meetmangukiya: So one of the challenges while implementing backoff retry or anything that would require us to make multiple rpc calls wouldn't work unless we make `param` either `Copy` or `Clone` which would require bounding `JsonRpcClient`'s `request` further and would be a breaking change.

```
error[E0276]: impl has stricter requirements than trait
  --> ethers-providers/src/transports/retry.rs:73:56
   |
73 |           A: std::fmt::Debug + Serialize + Send + Sync + Copy,
   |                                                          ^^^^ impl has extra requirement `A: std::marker::Copy`
   |
  ::: ethers-providers/src/lib.rs:63:5
   |
63 | /     async fn request<T, R>(&self, method: &str, params: T) -> Result<R, Self::Error>
64 | |     where
65 | |         T: Debug + Serialize + Send + Sync,
66 | |         R: DeserializeOwned;
   | |____________________________- definition of `request` from trait
```
meetmangukiya: @mattsse https://github.com/foundry-rs/foundry/runs/6608273536?check_suite_focus=true
mattsse: > @mattsse https://github.com/foundry-rs/foundry/runs/6608273536?check_suite_focus=true

very nice, lots of unrelated failing tests but no rate limit errors

also ref https://github.com/foundry-rs/foundry/pull/1728
meetmangukiya: > The last suggestion would be to serialize to string instead of serde_json::Value which prevents repeated serialization effort

if we serialize it to string, it actually gets serialized as a string further down when the json gets serialized, so instead of getting `{"params":["latest",null]}` we'd get `{"params":"[l\"latest\",null]"}` 
mattsse: > > The last suggestion would be to serialize to string instead of serde_json::Value which prevents repeated serialization effort
> 
> if we serialize it to string, it actually gets serialized as a string further down when the json gets serialized, so instead of getting `{"params":["latest",null]}` we'd get `{"params":"[l\"latest\",null]"}`

ah that makes sense, disregard that then!

Patch:
diff --git a/.cargo/audit.toml b/.cargo/audit.toml
new file mode 100644
index 00000000..c73dfd47
--- /dev/null
+++ b/.cargo/audit.toml
@@ -0,0 +1,4 @@
+[advisories]
+ignore = [
+    "RUSTSEC-2021-0127", # serde_cbor dependency through the criterion dev-dependency
+]
diff --git a/.github/workflows/audit-on-push.yml b/.github/workflows/audit-on-push.yml
new file mode 100644
index 00000000..e46bb3cc
--- /dev/null
+++ b/.github/workflows/audit-on-push.yml
@@ -0,0 +1,14 @@
+name: Security audit
+on:
+  push:
+    paths:
+      - '**/Cargo.toml'
+      - '**/Cargo.lock'
+jobs:
+  security_audit:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/audit-check@v1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8b2a0ac..8b517dd0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 ### Unreleased
 
+- Add `as_*_mut` methods on `TypedTransaction`
+  [#1310](https://github.com/gakonst/ethers-rs/pull/1310)
 - AWS EIP712 data signing no longer signs with EIP155
 - Added Cronos testnet to etherscan options [1276](https://github.com/gakonst/ethers-rs/pull/1276)
 - Fix parsing of a pending block
@@ -96,6 +98,8 @@
 
 ### Unreleased
 
+- Use relative source paths and `solc --base-path`
+  [#1317](https://github.com/gakonst/ethers-rs/pull/1317)
 - Save cache entry objects with relative paths
   [#1307](https://github.com/gakonst/ethers-rs/pull/1307)
 - Bundle svm, svm-builds and sha2 dependencies in new `svm-solc` feature
diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs
index ad4306d6..c2d57d16 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract.rs
@@ -49,7 +49,7 @@ impl ExpandedContract {
            // export all the created data types
             pub use #module::*;
 
-            #[allow(clippy::too_many_arguments)]
+            #[allow(clippy::too_many_arguments, non_camel_case_types)]
             mod #module {
                 #imports
                 #contract
diff --git a/ethers-core/src/abi/codec.rs b/ethers-core/src/abi/codec.rs
index 283bd076..d9f6c456 100644
--- a/ethers-core/src/abi/codec.rs
+++ b/ethers-core/src/abi/codec.rs
@@ -2,7 +2,7 @@ use crate::{
     abi::{
         AbiArrayType, AbiError, AbiType, Detokenize, Token, Tokenizable, TokenizableItem, Tokenize,
     },
-    types::{Address, Bytes, H256, U128, U256},
+    types::{Address, Bytes, H256, I256, U128, U256},
 };
 
 /// Trait for ABI encoding
@@ -63,6 +63,7 @@ impl_abi_codec!(
     H256,
     U128,
     U256,
+    I256,
     u8,
     u16,
     u32,
diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs
index b87f0abf..6c5fe479 100644
--- a/ethers-core/src/types/transaction/eip2718.rs
+++ b/ethers-core/src/types/transaction/eip2718.rs
@@ -417,6 +417,25 @@ impl TypedTransaction {
             _ => None,
         }
     }
+
+    pub fn as_legacy_mut(&mut self) -> Option<&mut TransactionRequest> {
+        match self {
+            Legacy(tx) => Some(tx),
+            _ => None,
+        }
+    }
+    pub fn as_eip2930_mut(&mut self) -> Option<&mut Eip2930TransactionRequest> {
+        match self {
+            Eip2930(tx) => Some(tx),
+            _ => None,
+        }
+    }
+    pub fn as_eip1559_mut(&mut self) -> Option<&mut Eip1559TransactionRequest> {
+        match self {
+            Eip1559(tx) => Some(tx),
+            _ => None,
+        }
+    }
 }
 
 impl TypedTransaction {
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index 213e3b54..77cc3ef0 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -58,7 +58,7 @@ wasm-timer = "0.2"
 parking_lot = { version = "0.11", features = ["wasm-bindgen"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
-tokio = { version = "1.5", default-features = false, features = ["rt", "macros"] }
+tokio = { version = "1.5", default-features = false, features = ["rt", "macros", "time"] }
 tempfile = "3.3.0"
 
 [features]
diff --git a/ethers-providers/src/pending_escalator.rs b/ethers-providers/src/pending_escalator.rs
index 3dca65fb..b813f986 100644
--- a/ethers-providers/src/pending_escalator.rs
+++ b/ethers-providers/src/pending_escalator.rs
@@ -190,7 +190,7 @@ where
                 poll_broadcast_fut!(cx, this, fut);
             }
             Sleeping(delay) => {
-                futures_util::ready!(delay.as_mut().poll(cx));
+                let _ready = futures_util::ready!(delay.as_mut().poll(cx));
                 // if broadcast timer has elapsed and if we have a TX to
                 // broadcast, broadcast it
                 if this.last.elapsed() > *this.broadcast_interval {
diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/pending_transaction.rs
index 68757aa8..17451b7e 100644
--- a/ethers-providers/src/pending_transaction.rs
+++ b/ethers-providers/src/pending_transaction.rs
@@ -172,7 +172,7 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
 
         match this.state {
             PendingTxState::InitialDelay(fut) => {
-                futures_util::ready!(fut.as_mut().poll(ctx));
+                let _ready = futures_util::ready!(fut.as_mut().poll(ctx));
                 tracing::debug!("Starting to poll pending tx {:?}", *this.tx_hash);
                 let fut = Box::pin(this.provider.get_transaction(*this.tx_hash));
                 rewake_with_new_state!(ctx, this, PendingTxState::GettingTx(fut));
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index a75e7cdf..e84d86f5 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -6,6 +6,9 @@ use crate::{
     PendingTransaction, QuorumProvider, RwClient, SyncingStatus,
 };
 
+#[cfg(not(target_arch = "wasm32"))]
+use crate::transports::{HttpRateLimitRetryPolicy, RetryClient};
+
 #[cfg(feature = "celo")]
 use crate::CeloMiddleware;
 use crate::Middleware;
@@ -1343,6 +1346,18 @@ impl<'a> TryFrom<&'a String> for Provider<HttpProvider> {
     }
 }
 
+#[cfg(not(target_arch = "wasm32"))]
+impl Provider<RetryClient<HttpProvider>> {
+    pub fn new_client(src: &str, max_retry: u32, initial_backoff: u64) -> Result<Self, ParseError> {
+        Ok(Provider::new(RetryClient::new(
+            HttpProvider::new(Url::parse(src)?),
+            Box::new(HttpRateLimitRetryPolicy),
+            max_retry,
+            initial_backoff,
+        )))
+    }
+}
+
 /// A middleware supporting development-specific JSON RPC methods
 ///
 /// # Example
diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs
index 1a369b40..6fc97dba 100644
--- a/ethers-providers/src/transports/mod.rs
+++ b/ethers-providers/src/transports/mod.rs
@@ -39,5 +39,10 @@ pub use quorum::{Quorum, QuorumError, QuorumProvider, WeightedProvider};
 mod rw;
 pub use rw::{RwClient, RwClientError};
 
+#[cfg(not(target_arch = "wasm32"))]
+mod retry;
+#[cfg(not(target_arch = "wasm32"))]
+pub use retry::*;
+
 mod mock;
 pub use mock::{MockError, MockProvider};
diff --git a/ethers-providers/src/transports/retry.rs b/ethers-providers/src/transports/retry.rs
new file mode 100644
index 00000000..17655c56
--- /dev/null
+++ b/ethers-providers/src/transports/retry.rs
@@ -0,0 +1,175 @@
+//! A [JsonRpcClient] implementation that retries requests filtered by [RetryPolicy]
+//! with an exponential backoff.
+
+use super::{common::JsonRpcError, http::ClientError};
+use crate::{provider::ProviderError, JsonRpcClient};
+
+use std::{
+    clone::Clone,
+    fmt::Debug,
+    sync::atomic::{AtomicU32, Ordering},
+    time::Duration,
+};
+
+use async_trait::async_trait;
+use serde::{de::DeserializeOwned, Serialize};
+use thiserror::Error;
+
+/// [RetryPolicy] defines logic for which [JsonRpcClient::Error] instances should
+/// the client retry the request and try to recover from.
+pub trait RetryPolicy<E>: Send + Sync + Debug {
+    fn should_retry(&self, error: &E) -> bool;
+}
+
+/// [RetryClient] presents as a wrapper around [JsonRpcClient] that will retry
+/// requests based with an exponential backoff and filtering based on [RetryPolicy].
+#[derive(Debug)]
+pub struct RetryClient<T>
+where
+    T: JsonRpcClient,
+    T::Error: Sync + Send + 'static,
+{
+    inner: T,
+    requests_enqueued: AtomicU32,
+    policy: Box<dyn RetryPolicy<T::Error>>,
+    max_retry: u32,
+    initial_backoff: u64,
+}
+
+impl<T> RetryClient<T>
+where
+    T: JsonRpcClient,
+    T::Error: Sync + Send + 'static,
+{
+    /// Example:
+    ///
+    /// ```no_run
+    /// use ethers_providers::{Http, RetryClient, HttpRateLimitRetryPolicy};
+    /// use std::time::Duration;
+    /// use url::Url;
+    ///
+    /// let http = Http::new(Url::parse("http://localhost:8545").unwrap());
+    /// let delay = Duration::new(10, 0);
+    /// let client = RetryClient::new(http, Box::new(HttpRateLimitRetryPolicy), 10, 1);
+    /// ```
+    pub fn new(
+        inner: T,
+        policy: Box<dyn RetryPolicy<T::Error>>,
+        max_retry: u32,
+        // in milliseconds
+        initial_backoff: u64,
+    ) -> Self {
+        Self { inner, requests_enqueued: AtomicU32::new(0), policy, max_retry, initial_backoff }
+    }
+}
+
+/// Error thrown when:
+/// 1. Internal client throws an error we do not wish to try to recover from.
+/// 2. Params serialization failed.
+/// 3. Request timed out i.e. max retries were already made.
+#[derive(Error, Debug)]
+pub enum RetryClientError<T>
+where
+    T: JsonRpcClient,
+    T::Error: Sync + Send + 'static,
+{
+    #[error(transparent)]
+    ProviderError(T::Error),
+    TimeoutError,
+    #[error(transparent)]
+    SerdeJson(serde_json::Error),
+}
+
+impl<T> std::fmt::Display for RetryClientError<T>
+where
+    T: JsonRpcClient,
+    <T as JsonRpcClient>::Error: Sync + Send + 'static,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:?}", self)
+    }
+}
+
+impl<T> From<RetryClientError<T>> for ProviderError
+where
+    T: JsonRpcClient + 'static,
+    <T as JsonRpcClient>::Error: Sync + Send + 'static,
+{
+    fn from(src: RetryClientError<T>) -> Self {
+        ProviderError::JsonRpcClientError(Box::new(src))
+    }
+}
+
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl<T> JsonRpcClient for RetryClient<T>
+where
+    T: JsonRpcClient + 'static,
+    T::Error: Sync + Send + 'static,
+{
+    type Error = RetryClientError<T>;
+
+    async fn request<A, R>(&self, method: &str, params: A) -> Result<R, Self::Error>
+    where
+        A: std::fmt::Debug + Serialize + Send + Sync,
+        R: DeserializeOwned,
+    {
+        self.requests_enqueued.fetch_add(1, Ordering::SeqCst);
+
+        let params =
+            serde_json::to_value(params).map_err(|err| RetryClientError::SerdeJson(err))?;
+
+        let mut retry_number: u32 = 0;
+
+        loop {
+            let err;
+
+            // hack to not hold `R` across an await in the sleep future and prevent requiring
+            // R: Send + Sync
+            {
+                match self.inner.request(method, params.clone()).await {
+                    Ok(ret) => {
+                        self.requests_enqueued.fetch_sub(1, Ordering::SeqCst);
+                        return Ok(ret)
+                    }
+                    Err(err_) => err = err_,
+                }
+            }
+
+            retry_number += 1;
+            if retry_number > self.max_retry {
+                return Err(RetryClientError::TimeoutError)
+            }
+
+            let should_retry = self.policy.should_retry(&err);
+            if should_retry {
+                let current_queued_requests = self.requests_enqueued.load(Ordering::SeqCst);
+                // using `retry_number + current_queued_requests` for creating back pressure because
+                // of already queued requests
+                let next_backoff =
+                    self.initial_backoff * 2u64.pow(retry_number + current_queued_requests);
+                tokio::time::sleep(Duration::from_millis(next_backoff)).await;
+            } else {
+                self.requests_enqueued.fetch_sub(1, Ordering::SeqCst);
+                return Err(RetryClientError::ProviderError(err))
+            }
+        }
+    }
+}
+
+/// Implements [RetryPolicy] that will retry requests that errored with
+/// status code 429 i.e. TOO_MANY_REQUESTS
+#[derive(Debug)]
+pub struct HttpRateLimitRetryPolicy;
+
+impl RetryPolicy<ClientError> for HttpRateLimitRetryPolicy {
+    fn should_retry(&self, error: &ClientError) -> bool {
+        match error {
+            ClientError::ReqwestError(err) => {
+                err.status() == Some(http::StatusCode::TOO_MANY_REQUESTS)
+            }
+            // alchemy throws it this way
+            ClientError::JsonRpcError(JsonRpcError { code, message: _, data: _ }) => *code == 429,
+            _ => false,
+        }
+    }
+}
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 3a8c6ffc..732f5956 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -1,21 +1,18 @@
 //! Solc artifact types
-use ethers_core::abi::Abi;
-
+use crate::{
+    compile::*, error::SolcIoError, remappings::Remapping, utils, ProjectPathsConfig, SolcError,
+};
 use colored::Colorize;
+use ethers_core::abi::Abi;
 use md5::Digest;
 use semver::{Version, VersionReq};
+use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
 use std::{
     collections::{BTreeMap, HashSet},
     fmt, fs,
     path::{Path, PathBuf},
     str::FromStr,
 };
-
-use crate::{
-    compile::*, error::SolcIoError, remappings::Remapping, utils, ProjectPathsConfig, SolcError,
-};
-
-use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
 use tracing::warn;
 
 pub mod ast;
@@ -189,10 +186,20 @@ impl CompilerInput {
         self.sources = self
             .sources
             .into_iter()
-            .map(|(path, s)| (path.strip_prefix(base).map(|p| p.to_path_buf()).unwrap_or(path), s))
+            .map(|(path, s)| (path.strip_prefix(base).map(Into::into).unwrap_or(path), s))
             .collect();
         self
     }
+
+    /// Similar to `Self::strip_prefix()`. Remove a base path from all
+    /// sources _and_ all paths in solc settings such as remappings
+    ///
+    /// See also `solc --base-path`
+    pub fn with_base_path(mut self, base: impl AsRef<Path>) -> Self {
+        let base = base.as_ref();
+        self.settings = self.settings.with_base_path(base);
+        self.strip_prefix(base)
+    }
 }
 
 /// A `CompilerInput` representation used for verify
@@ -385,6 +392,38 @@ impl Settings {
         output.insert("".to_string(), vec!["ast".to_string()]);
         self
     }
+
+    /// Strips `base` from all paths
+    pub fn with_base_path(mut self, base: impl AsRef<Path>) -> Self {
+        let base = base.as_ref();
+        self.remappings.iter_mut().for_each(|r| {
+            r.strip_prefix(base);
+        });
+
+        self.libraries.libs = self
+            .libraries
+            .libs
+            .into_iter()
+            .map(|(file, libs)| (file.strip_prefix(base).map(Into::into).unwrap_or(file), libs))
+            .collect();
+
+        self.output_selection = OutputSelection(
+            self.output_selection
+                .0
+                .into_iter()
+                .map(|(file, selection)| {
+                    (
+                        Path::new(&file)
+                            .strip_prefix(base)
+                            .map(|p| format!("{}", p.display()))
+                            .unwrap_or(file),
+                        selection,
+                    )
+                })
+                .collect(),
+        );
+        self
+    }
 }
 
 impl Default for Settings {
diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs
index 7c3a77fd..1476ecd0 100644
--- a/ethers-solc/src/compile/mod.rs
+++ b/ethers-solc/src/compile/mod.rs
@@ -127,6 +127,8 @@ impl fmt::Display for SolcVersion {
 pub struct Solc {
     /// Path to the `solc` executable
     pub solc: PathBuf,
+    /// The base path to set when invoking solc, see also <https://docs.soliditylang.org/en/v0.8.11/path-resolution.html#base-path-and-include-paths>
+    pub base_path: Option<PathBuf>,
     /// Additional arguments passed to the `solc` exectuable
     pub args: Vec<String>,
 }
@@ -163,7 +165,15 @@ impl fmt::Display for Solc {
 impl Solc {
     /// A new instance which points to `solc`
     pub fn new(path: impl Into<PathBuf>) -> Self {
-        Solc { solc: path.into(), args: Vec::new() }
+        Solc { solc: path.into(), base_path: None, args: Vec::new() }
+    }
+
+    /// Sets solc's base path
+    ///
+    /// Ref: <https://docs.soliditylang.org/en/v0.8.11/path-resolution.html#base-path-and-include-paths>
+    pub fn with_base_path(mut self, base_path: impl Into<PathBuf>) -> Self {
+        self.base_path = Some(base_path.into());
+        self
     }
 
     /// Adds an argument to pass to the `solc` command.
@@ -513,6 +523,9 @@ impl Solc {
 
     pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
         let mut cmd = Command::new(&self.solc);
+        if let Some(ref base_path) = self.base_path {
+            cmd.current_dir(base_path);
+        }
         let mut child = cmd
             .args(&self.args)
             .arg("--standard-json")
@@ -575,7 +588,11 @@ 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.solc)
+        let mut cmd = tokio::process::Command::new(&self.solc);
+        if let Some(ref base_path) = self.base_path {
+            cmd.current_dir(base_path);
+        }
+        let mut child = cmd
             .args(&self.args)
             .arg("--standard-json")
             .stdin(Stdio::piped())
diff --git a/ethers-solc/src/compile/output/contracts.rs b/ethers-solc/src/compile/output/contracts.rs
index 2945cd19..ed1f0069 100644
--- a/ethers-solc/src/compile/output/contracts.rs
+++ b/ethers-solc/src/compile/output/contracts.rs
@@ -136,7 +136,7 @@ impl VersionedContracts {
         self.0 = std::mem::take(&mut self.0)
             .into_iter()
             .map(|(contract_path, contracts)| {
-                (root.join(contract_path).to_string_lossy().to_string(), contracts)
+                (format!("{}", root.join(contract_path).display()), contracts)
             })
             .collect();
         self
diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs
index e6e0a61c..508ffb0e 100644
--- a/ethers-solc/src/compile/output/mod.rs
+++ b/ethers-solc/src/compile/output/mod.rs
@@ -366,6 +366,14 @@ impl AggregatedCompilerOutput {
         (self.sources, self.contracts)
     }
 
+    /// Joins all file path with `root`
+    pub fn join_all(&mut self, root: impl AsRef<Path>) -> &mut Self {
+        let root = root.as_ref();
+        self.contracts.join_all(root);
+        self.sources.join_all(root);
+        self
+    }
+
     /// Strips the given prefix from all file paths to make them relative to the given
     /// `base` argument.
     ///
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index 59b6774e..f25f7e7e 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -106,6 +106,7 @@ use crate::{
     artifacts::{Settings, VersionedFilteredSources, VersionedSources},
     cache::ArtifactsCache,
     error::Result,
+    filter::SparseOutputFilter,
     output::AggregatedCompilerOutput,
     report,
     resolver::GraphEdges,
@@ -113,8 +114,6 @@ use crate::{
     Sources,
 };
 use rayon::prelude::*;
-
-use crate::filter::SparseOutputFilter;
 use std::{collections::btree_map::BTreeMap, path::PathBuf, time::Instant};
 
 #[derive(Debug)]
@@ -155,7 +154,9 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
     pub fn with_sources(project: &'a Project<T>, sources: Sources) -> Result<Self> {
         let graph = Graph::resolve_sources(&project.paths, sources)?;
         let (versions, edges) = graph.into_sources_by_version(project.offline)?;
-        let sources_by_version = versions.get(&project.allowed_lib_paths)?;
+
+        let base_path = project.root();
+        let sources_by_version = versions.get(&project.allowed_lib_paths, base_path)?;
 
         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
@@ -239,13 +240,20 @@ impl<'a, T: ArtifactOutput> PreprocessedState<'a, T> {
     /// advance to the next state by compiling all sources
     fn compile(self) -> Result<CompiledState<'a, T>> {
         let PreprocessedState { sources, cache, sparse_output } = self;
-        let output = sources.compile(
+        let mut output = sources.compile(
             &cache.project().solc_config.settings,
             &cache.project().paths,
             sparse_output,
             cache.graph(),
         )?;
 
+        // source paths get stripped before handing them over to solc, so solc never uses absolute
+        // paths, instead `--base-path <root dir>` is set. this way any metadata that's derived from
+        // data (paths) is relative to the project dir and should be independent of the current OS
+        // disk. However internally we still want to keep absolute paths, so we join the
+        // contracts again
+        output.join_all(cache.project().root());
+
         Ok(CompiledState { output, cache })
     }
 }
@@ -457,9 +465,10 @@ fn compile_sequential(
                 continue
             }
             let input = input
-                .settings(opt_settings.clone())
+                .settings(opt_settings.clone().with_base_path(&paths.root))
                 .normalize_evm_version(&version)
                 .with_remappings(paths.remappings.clone())
+                .with_base_path(&paths.root)
                 .sanitized(&version);
 
             tracing::trace!(
@@ -539,6 +548,7 @@ fn compile_parallel(
                 .settings(settings.clone())
                 .normalize_evm_version(&version)
                 .with_remappings(paths.remappings.clone())
+                .with_base_path(&paths.root)
                 .sanitized(&version);
 
             jobs.push((solc.clone(), version.clone(), job, actually_dirty))
@@ -740,7 +750,6 @@ mod tests {
             .unwrap();
         let project = Project::builder().paths(paths).build().unwrap();
         let compiler = ProjectCompiler::new(&project).unwrap();
-        let out = compiler.compile().unwrap();
-        println!("{}", out);
+        let _out = compiler.compile().unwrap();
     }
 }
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index bf6c41fc..58e4dd8a 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -141,10 +141,12 @@ impl<T: ArtifactOutput> Project<T> {
     ///
     /// This will set the `--allow-paths` to the paths configured for the `Project`, if any.
     fn configure_solc(&self, mut solc: Solc) -> Solc {
-        if solc.args.is_empty() && !self.allowed_lib_paths.0.is_empty() {
+        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());
         }
-        solc
+        solc.with_base_path(self.root())
     }
 
     /// Sets the maximum number of parallel `solc` processes to run simultaneously.
diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs
index e8902673..7c2382bd 100644
--- a/ethers-solc/src/remappings.rs
+++ b/ethers-solc/src/remappings.rs
@@ -55,6 +55,14 @@ impl Remapping {
     pub fn into_relative(self, root: impl AsRef<Path>) -> RelativeRemapping {
         RelativeRemapping::new(self, root)
     }
+
+    /// Removes the `base` path from the remapping
+    pub fn strip_prefix(&mut self, base: impl AsRef<Path>) -> &mut Self {
+        if let Ok(stripped) = Path::new(&self.path).strip_prefix(base.as_ref()) {
+            self.path = format!("{}", stripped.display());
+        }
+        self
+    }
 }
 
 #[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd)]
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 9183fd0b..15fb77fb 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -701,12 +701,21 @@ pub struct VersionedSources {
 #[cfg(all(feature = "svm-solc"))]
 impl VersionedSources {
     /// Resolves or installs the corresponding `Solc` installation.
+    ///
+    /// This will also configure following solc arguments:
+    ///    - `allowed_paths`
+    ///    - `base_path`
     pub fn get(
         self,
         allowed_lib_paths: &crate::AllowedLibPaths,
+        base_path: impl AsRef<Path>,
     ) -> 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();
@@ -743,8 +752,16 @@ impl VersionedSources {
                     tracing::trace!("reinstalled solc: \"{}\"", version);
                 }
             }
-            let solc = solc.arg("--allow-paths").arg(allowed_lib_paths.to_string());
+            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()));
+            }
+
             sources_by_version.insert(solc, (version, sources));
         }
         Ok(sources_by_version)


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 8da70148..5aaad5aa 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -1434,7 +1434,6 @@ fn can_detect_contract_def_source_files() {
         .unwrap();
 
     let compiled = tmp.compile().unwrap();
-    println!("{}", compiled);
     assert!(!compiled.has_compiler_errors());
 
     let mut sources = compiled.output().sources;
@@ -1750,7 +1749,6 @@ contract D { }
         .unwrap();
 
     let compiled = project.compile().unwrap();
-    println!("{}", compiled);
     assert!(!compiled.has_compiler_errors());
 
     let cache = SolFilesCache::read(project.cache_path()).unwrap();