Repository: gakonst/ethers-rs
Pull request #1613 (2022-08-18 19:25:12)

fix(providers): QuorumProvider zero-parameter json Value handling
<!--
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

<!--
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.
-->

Currently the `QuorumProvider` fails for the very simple case of `provider.request("eth_blockNumber", ())`.

Example code which fails:
```rs
use ethers::prelude::{Http, JsonRpcClient, Quorum, QuorumProvider, WeightedProvider, U64};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let provider1 = "http://127.0.0.1:8545".parse::<Http>().unwrap();
    let provider2 = "http://127.0.0.1:8545".parse::<Http>().unwrap();
    let provider3 = "http://127.0.0.1:8545".parse::<Http>().unwrap();
    let quorum_provider = QuorumProvider::builder()
        .add_providers(
            [&provider1, &provider2, &provider3]
                .into_iter()
                .cloned()
                .map(WeightedProvider::new),
        )
        .quorum(Quorum::Majority)
        .build();

    let block_number: U64 = quorum_provider
        .request("eth_blockNumber", ())
        .await
        .expect("Failed to get block number");
    println!("Quorum block number {block_number}");
}
```

Ran against the hardhat node.
```
$ yarn hardhat node
```

Bit about my env:
```
$ yarn hardhat --version
2.9.9
```

The error message observed from the above code is:
```
thread 'main' panicked at 'Failed to get block number: JsonRpcClientError(NoQuorumReached { values: [], errors: [JsonRpcClientError(SerdeJson { err: Error("invalid type: null, expected u64", line: 1, column: 26), text: "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Invalid request\",\"data\":{\"message\":\"Invalid request\"}}}" }), JsonRpcClientError(SerdeJson { err: Error("invalid type: null, expected u64", line: 1, column: 26), text: "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Invalid request\",\"data\":{\"message\":\"Invalid request\"}}}" }), JsonRpcClientError(SerdeJson { err: Error("invalid type: null, expected u64", line: 1, column: 26), text: "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Invalid request\",\"data\":{\"message\":\"Invalid request\"}}}" })] })'
```

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

After much digging into why this was an issue with `QuorumProvider` and not also with `Http`, I noticed that the http provider is using the following to serialize the body.
```rust
fn is_zst<T>(_t: &T) -> bool {
    std::mem::size_of::<T>() == 0
}

#[derive(Serialize, Deserialize, Debug)]
/// A JSON-RPC request
pub struct Request<'a, T> {
    id: u64,
    jsonrpc: &'a str,
    method: &'a str,
    #[serde(skip_serializing_if = "is_zst")]
    params: T,
}
```

What happens instead with `QuorumProvider` is it is using `serde_json::Value` types to support multiple different backends through the `JsonRpcClientWrapper` which defines something like:
```rust
impl<C: JsonRpcClient> JsonRpcClientWrapper for C {
    async fn request(&self, method: &str, params: Value) -> Result<Value, ProviderError> {
        Ok(JsonRpcClient::request(self, method, params).await.map_err(C::Error::into)?)
    }
}
```

the consequence is that when `params` is `()` it will be serialized as `serde_json::Value::Null` instead of being excluded from the body. Some providers may accept this as valid, but since it is not working with the hardhat node we need it changed at a minimum for our local code tests. The two solutions I saw for this are to:

1) Make it an Option<Value> which can be excluded from the body
2) Use an empty array

~~This PR opts for option (2) as it seems to be the minimal change.~~
See discussion below.

## PR Checklist

- [ ] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 626ae469..49a7e13f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -117,6 +117,8 @@
 
 ### Unreleased
 
+- Add `OutputContext` to `ArtifactOutput` trait
+  [#1621](https://github.com/gakonst/ethers-rs/pull/1621)
 - On windows all paths in the `ProjectCompilerOutput` are now slashed by default
   [#1540](https://github.com/gakonst/ethers-rs/pull/1540)
 - `ArtifactOutput::write_extras` now takes the `Artifacts` directly
diff --git a/Cargo.lock b/Cargo.lock
index cad1d54e..b473d7b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -135,6 +135,18 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "auto_impl"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "auto_impl"
 version = "1.0.1"
@@ -1365,6 +1377,7 @@ name = "ethers-middleware"
 version = "0.17.0"
 dependencies = [
  "async-trait",
+ "auto_impl 0.5.0",
  "ethers-contract",
  "ethers-core",
  "ethers-etherscan",
@@ -1393,7 +1406,7 @@ name = "ethers-providers"
 version = "0.17.0"
 dependencies = [
  "async-trait",
- "auto_impl",
+ "auto_impl 1.0.1",
  "base64 0.13.0",
  "bytes",
  "ethers-core",
@@ -1547,7 +1560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "089263294bb1c38ac73649a6ad563dd9a5142c8dc0482be15b8b9acb22a1611e"
 dependencies = [
  "arrayvec 0.7.2",
- "auto_impl",
+ "auto_impl 1.0.1",
  "bytes",
  "ethereum-types",
  "fastrlp-derive",
@@ -3767,9 +3780,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 
 [[package]]
 name = "svm-rs"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d3173bcc4e5c210a147bd6d61becd3cc053517bf38253bd93c376ae7d818172"
+checksum = "a9da7be4aa3d6b944579b66af0cfdca48f2682de7d5b85cf40449805e2e93db9"
 dependencies = [
  "anyhow",
  "cfg-if 1.0.0",
@@ -3798,9 +3811,9 @@ dependencies = [
 
 [[package]]
 name = "svm-rs-builds"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9667365ef795e87d08cb456815d028ce96bfdcfd7b9aaa2bb461766e2e4a738b"
+checksum = "a8d68b31f727e62896d9c1d375f169ca3b4e3ba4c8fa5ab23eaf4de244bb43d3"
 dependencies = [
  "build_const",
  "hex",
diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs
index 8dcc4fba..1f753414 100644
--- a/ethers-contract/src/call.rs
+++ b/ethers-contract/src/call.rs
@@ -150,7 +150,7 @@ where
 
     /// Returns the estimated gas cost for the underlying transaction to be executed
     pub async fn estimate_gas(&self) -> Result<U256, ContractError<M>> {
-        self.client.estimate_gas(&self.tx).await.map_err(ContractError::MiddlewareError)
+        self.client.estimate_gas(&self.tx, self.block).await.map_err(ContractError::MiddlewareError)
     }
 
     /// Queries the blockchain via an `eth_call` for the provided transaction.
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index 6358c3ae..0a51182c 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -41,7 +41,7 @@ cargo_metadata = { version = "0.15.0", optional = true }
 convert_case = { version = "0.5.0", optional = true }
 syn = { version = "1.0.99", optional = true }
 proc-macro2 = { version = "1.0.43", optional = true }
-rust_decimal = "1.26.1"
+rust_decimal = { version = "1.26.1", features = ["maths"] }
 
 [dev-dependencies]
 serde_json = { version = "1.0.64", default-features = false }
diff --git a/ethers-core/src/types/filter.rs b/ethers-core/src/types/filter.rs
index 077b6045..04f750da 100644
--- a/ethers-core/src/types/filter.rs
+++ b/ethers-core/src/types/filter.rs
@@ -254,13 +254,21 @@ impl Filter {
         self
     }
 
-    /// given the event in string form, it hashes it and adds it to the topics to monitor
+    /// Given the event signature in string form, it hashes it and adds it to the topics to monitor
     #[must_use]
     pub fn event(self, event_name: &str) -> Self {
         let hash = H256::from(keccak256(event_name.as_bytes()));
         self.topic0(hash)
     }
 
+    /// Hashes all event signatures and sets them as array to topic0
+    #[must_use]
+    pub fn events(self, events: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Self {
+        let events =
+            events.into_iter().map(|e| H256::from(keccak256(e.as_ref()))).collect::<Vec<_>>();
+        self.topic0(events)
+    }
+
     /// Sets topic0 (the event name for non-anonymous events)
     #[must_use]
     pub fn topic0<T: Into<Topic>>(mut self, topic: T) -> Self {
@@ -544,6 +552,12 @@ impl From<H256> for Topic {
     }
 }
 
+impl From<Vec<H256>> for ValueOrArray<H256> {
+    fn from(src: Vec<H256>) -> Self {
+        ValueOrArray::Array(src)
+    }
+}
+
 impl From<ValueOrArray<H256>> for Topic {
     fn from(src: ValueOrArray<H256>) -> Self {
         match src {
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index a5703e95..c03e9a7a 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -68,3 +68,6 @@ mod other;
 pub use other::OtherFields;
 
 pub mod serde_helpers;
+
+mod syncing;
+pub use syncing::{SyncProgress, SyncingStatus};
diff --git a/ethers-core/src/types/proof.rs b/ethers-core/src/types/proof.rs
index 011c3576..a1d9cc17 100644
--- a/ethers-core/src/types/proof.rs
+++ b/ethers-core/src/types/proof.rs
@@ -1,4 +1,4 @@
-use crate::types::{Address, Bytes, H256, U256};
+use crate::types::{Address, Bytes, H256, U256, U64};
 use serde::{Deserialize, Serialize};
 
 #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
@@ -11,13 +11,13 @@ pub struct StorageProof {
 #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct EIP1186ProofResponse {
-    address: Address,
-    balance: U256,
-    code_hash: H256,
-    nonce: U256,
-    storage_hash: H256,
-    account_proof: Vec<Bytes>,
-    storage_proof: Vec<StorageProof>,
+    pub address: Address,
+    pub balance: U256,
+    pub code_hash: H256,
+    pub nonce: U64,
+    pub storage_hash: H256,
+    pub account_proof: Vec<Bytes>,
+    pub storage_proof: Vec<StorageProof>,
 }
 
 #[cfg(test)]
diff --git a/ethers-core/src/types/syncing.rs b/ethers-core/src/types/syncing.rs
new file mode 100644
index 00000000..58e685a6
--- /dev/null
+++ b/ethers-core/src/types/syncing.rs
@@ -0,0 +1,164 @@
+//! Types for `eth_syncing` RPC call
+
+use crate::types::U64;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+/// Structure used in `eth_syncing` RPC
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum SyncingStatus {
+    /// When client is synced to highest block, eth_syncing with return string "false"
+    IsFalse,
+    /// When client is still syncing past blocks we get IsSyncing information.
+    IsSyncing(Box<SyncProgress>),
+}
+
+impl Serialize for SyncingStatus {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        match self {
+            SyncingStatus::IsFalse => serializer.serialize_bool(false),
+            SyncingStatus::IsSyncing(sync) => sync.serialize(serializer),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for SyncingStatus {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        #[derive(Debug, Serialize, Deserialize)]
+        #[serde(untagged)]
+        pub enum SyncingStatusIntermediate {
+            /// When client is synced to the highest block, eth_syncing with return string "false"
+            IsFalse(bool),
+            /// When client is still syncing past blocks we get IsSyncing information.
+            IsSyncing(Box<SyncProgress>),
+        }
+
+        match SyncingStatusIntermediate::deserialize(deserializer)? {
+            SyncingStatusIntermediate::IsFalse(false) => Ok(SyncingStatus::IsFalse),
+            SyncingStatusIntermediate::IsFalse(true) => Err(serde::de::Error::custom(
+                "eth_syncing returned `true` that is undefined value.",
+            )),
+            SyncingStatusIntermediate::IsSyncing(sync) => Ok(SyncingStatus::IsSyncing(sync)),
+        }
+    }
+}
+
+/// Represents the sync status of the node
+///
+/// **Note:** while the `eth_syncing` RPC response is defined as:
+///
+/// > Returns:
+/// >
+/// > Object|Boolean, An object with sync status data or FALSE, when not syncing:
+///
+/// > startingBlock: QUANTITY - The block at which the import started (will only be reset, after the
+/// > sync reached his head)
+/// > currentBlock: QUANTITY - The current block, same as eth_blockNumber
+/// > highestBlock: QUANTITY - The estimated highest block
+///
+/// Geth returns additional fields: <https://github.com/ethereum/go-ethereum/blob/0ce494b60cd00d70f1f9f2dd0b9bfbd76204168a/ethclient/ethclient.go#L597-L617>
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SyncProgress {
+    pub current_block: U64,
+    pub highest_block: U64,
+    pub starting_block: U64,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub pulled_states: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub known_states: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_bytecode_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_bytecodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_trienode_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_trienodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healing_bytecode: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healing_trienodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_account_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_accounts: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_bytecode_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_bytecodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_storage: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_storage_bytes: Option<U64>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // <https://github.com/gakonst/ethers-rs/issues/1623>
+    #[test]
+    fn deserialize_sync_geth() {
+        let s = r#"{
+        "currentBlock": "0xeaa2b4",
+        "healedBytecodeBytes": "0xaad91fe",
+        "healedBytecodes": "0x61d3",
+        "healedTrienodeBytes": "0x156ac02b1",
+        "healedTrienodes": "0x2885aa4",
+        "healingBytecode": "0x0",
+        "healingTrienodes": "0x454",
+        "highestBlock": "0xeaa329",
+        "startingBlock": "0xea97ee",
+        "syncedAccountBytes": "0xa29fec90d",
+        "syncedAccounts": "0xa7ed9ad",
+        "syncedBytecodeBytes": "0xdec39008",
+        "syncedBytecodes": "0x8d407",
+        "syncedStorage": "0x2a517da1",
+        "syncedStorageBytes": "0x23634dbedf"
+    }"#;
+
+        let sync: SyncingStatus = serde_json::from_str(s).unwrap();
+        match sync {
+            SyncingStatus::IsFalse => {
+                panic!("unexpected variant")
+            }
+            SyncingStatus::IsSyncing(_) => {}
+        }
+    }
+
+    #[test]
+    fn deserialize_sync_minimal() {
+        let s = r#"{
+        "currentBlock": "0xeaa2b4",
+        "highestBlock": "0xeaa329",
+        "startingBlock": "0xea97ee"
+    }"#;
+
+        let sync: SyncingStatus = serde_json::from_str(s).unwrap();
+        match sync {
+            SyncingStatus::IsFalse => {
+                panic!("unexpected variant")
+            }
+            SyncingStatus::IsSyncing(_) => {}
+        }
+    }
+
+    #[test]
+    fn deserialize_sync_false() {
+        let s = r#"false"#;
+
+        let sync: SyncingStatus = serde_json::from_str(s).unwrap();
+        match sync {
+            SyncingStatus::IsFalse => {}
+            SyncingStatus::IsSyncing(_) => {
+                panic!("unexpected variant")
+            }
+        }
+    }
+}
diff --git a/ethers-core/src/types/trace/geth.rs b/ethers-core/src/types/trace/geth.rs
index 786099c9..5cb38509 100644
--- a/ethers-core/src/types/trace/geth.rs
+++ b/ethers-core/src/types/trace/geth.rs
@@ -1,29 +1,37 @@
-use crate::types::{Bytes, H256, U256};
+use crate::types::{Bytes, H160, H256, U256};
 use serde::{Deserialize, Serialize};
 use std::collections::BTreeMap;
 
+// https://github.com/ethereum/go-ethereum/blob/a9ef135e2dd53682d106c6a2aede9187026cc1de/eth/tracers/logger/logger.go#L406-L411
 #[derive(Serialize, Deserialize, Debug)]
 pub struct GethTrace {
-    failed: bool,
-    gas: u64,
+    pub failed: bool,
+    pub gas: u64,
     #[serde(rename = "returnValue")]
-    return_value: Bytes,
+    pub return_value: Bytes,
     #[serde(rename = "structLogs")]
-    struct_logs: Vec<StructLog>,
+    pub struct_logs: Vec<StructLog>,
 }
 
+// https://github.com/ethereum/go-ethereum/blob/366d2169fbc0e0f803b68c042b77b6b480836dbc/eth/tracers/logger/logger.go#L413-L426
 #[derive(Serialize, Deserialize, Debug)]
 pub struct StructLog {
-    depth: u64,
-    error: Option<String>,
-    gas: u64,
+    pub depth: u64,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub error: Option<String>,
+    pub gas: u64,
     #[serde(rename = "gasCost")]
-    gas_cost: u64,
-    memory: Option<Vec<String>>,
-    op: String,
-    pc: U256,
-    stack: Vec<String>,
-    storage: BTreeMap<H256, H256>,
+    pub gas_cost: u64,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub memory: Option<Vec<u8>>,
+    pub op: String,
+    pub pc: U256,
+    #[serde(rename = "refund", skip_serializing_if = "Option::is_none")]
+    pub refund_counter: Option<u64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stack: Option<Vec<U256>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub storage: Option<BTreeMap<H160, BTreeMap<H256, H256>>>,
 }
 
 /// Bindings for additional `debug_traceTransaction` options
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index b1cf9ca2..5e3b1c2d 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -155,9 +155,15 @@ where
     S: ToString,
     K: TryInto<Units, Error = ConversionError> + Copy,
 {
-    use rust_decimal::Decimal;
+    use rust_decimal::{Decimal, MathematicalOps};
+
     let num: Decimal = amount.to_string().parse()?;
-    let multiplier: Decimal = 10u64.pow(units.try_into()?.as_num()).into();
+    let exponent = units.try_into()?.as_num();
+
+    let multiplier = Decimal::TEN
+        .checked_powu(exponent.into())
+        .ok_or(rust_decimal::Error::ExceedsMaximumPossibleValue)?;
+
     let val =
         num.checked_mul(multiplier).ok_or(rust_decimal::Error::ExceedsMaximumPossibleValue)?;
     let u256_n: U256 = U256::from_dec_str(&val.round().to_string())?;
@@ -441,6 +447,14 @@ mod tests {
         assert_eq!(eth, "1.005633240123456789");
     }
 
+    #[test]
+    fn parse_large_units() {
+        let decimals = 27u32;
+        let val = "10.55";
+        let unit = parse_units(val, decimals).unwrap();
+        assert_eq!(unit.to_string(), "10550000000000000000000000000");
+    }
+
     #[test]
     fn test_parse_units() {
         let gwei = parse_units(1.5, 9).unwrap();
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 6a7bc461..0544e54b 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -21,6 +21,7 @@ ethers-providers = { version = "^0.17.0", path = "../ethers-providers", default-
 ethers-signers = { version = "^0.17.0", path = "../ethers-signers", default-features = false }
 
 async-trait = { version = "0.1.50", default-features = false }
+auto_impl = { version = "0.5.0", default-features = false }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0", default-features = false }
 futures-util = { version = "^0.3" }
diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs
index a1444ff1..65ddc46c 100644
--- a/ethers-middleware/src/gas_oracle/gas_now.rs
+++ b/ethers-middleware/src/gas_oracle/gas_now.rs
@@ -23,7 +23,7 @@ struct GasNowResponseWrapper {
     data: GasNowResponse,
 }
 
-#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
 pub struct GasNowResponse {
     pub rapid: u64,
     pub fast: u64,
@@ -41,7 +41,7 @@ impl GasNow {
     pub fn with_client(client: Client) -> Self {
         let url = Url::parse(GAS_NOW_URL).expect("invalid url");
 
-        Self { url, gas_category: GasCategory::Standard }
+        Self { client, url, gas_category: GasCategory::Standard }
     }
 
     /// Sets the gas price category to be used when fetching the gas price.
@@ -64,7 +64,7 @@ impl GasNow {
 
 impl Default for GasNow {
     fn default() -> Self {
-        Self::new(Client::new())
+        Self::new()
     }
 }
 
diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs
index 844a5169..272b0150 100644
--- a/ethers-middleware/src/gas_oracle/middleware.rs
+++ b/ethers-middleware/src/gas_oracle/middleware.rs
@@ -56,24 +56,11 @@ where
         &self.inner
     }
 
-    async fn get_gas_price(&self) -> Result<U256, Self::Error> {
-        Ok(self.gas_oracle.fetch().await?)
-    }
-
-    async fn estimate_eip1559_fees(
-        &self,
-        _: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
-    ) -> Result<(U256, U256), Self::Error> {
-        Ok(self.gas_oracle.estimate_eip1559_fees().await?)
-    }
-
-    async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
+    async fn fill_transaction(
         &self,
-        tx: T,
+        tx: &mut TypedTransaction,
         block: Option<BlockId>,
-    ) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
-        let mut tx = tx.into();
-
+    ) -> Result<(), Self::Error> {
         match tx {
             TypedTransaction::Legacy(ref mut tx) => {
                 if tx.gas_price.is_none() {
@@ -98,6 +85,28 @@ where
                 }
             }
         };
+
+        self.inner().fill_transaction(tx, block).await.map_err(FromErr::from)
+    }
+
+    async fn get_gas_price(&self) -> Result<U256, Self::Error> {
+        Ok(self.gas_oracle.fetch().await?)
+    }
+
+    async fn estimate_eip1559_fees(
+        &self,
+        _: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
+    ) -> Result<(U256, U256), Self::Error> {
+        Ok(self.gas_oracle.estimate_eip1559_fees().await?)
+    }
+
+    async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
+        &self,
+        tx: T,
+        block: Option<BlockId>,
+    ) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
+        let mut tx = tx.into();
+        self.fill_transaction(&mut tx, block).await?;
         self.inner.send_transaction(tx, block).await.map_err(MiddlewareError::MiddlewareError)
     }
 }
diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs
index 155c277c..4a7a701c 100644
--- a/ethers-middleware/src/gas_oracle/mod.rs
+++ b/ethers-middleware/src/gas_oracle/mod.rs
@@ -22,10 +22,18 @@ pub use cache::Cache;
 mod polygon;
 pub use polygon::Polygon;
 
+mod gas_now;
+pub use gas_now::GasNow;
+
+mod provider_oracle;
+pub use provider_oracle::ProviderOracle;
+
 use ethers_core::types::U256;
 
 use async_trait::async_trait;
+use auto_impl::auto_impl;
 use reqwest::Error as ReqwestError;
+use std::error::Error;
 use thiserror::Error;
 
 const GWEI_TO_WEI: u64 = 1000000000;
@@ -73,6 +81,10 @@ pub enum GasOracleError {
 
     #[error("Chain is not supported by the oracle")]
     UnsupportedChain,
+
+    /// Error thrown when the provider failed.
+    #[error("Chain is not supported by the oracle")]
+    ProviderError(#[from] Box<dyn Error + Send + Sync>),
 }
 
 /// `GasOracle` is a trait that an underlying gas oracle needs to implement.
@@ -95,6 +107,7 @@ pub enum GasOracleError {
 /// ```
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+#[auto_impl(&, Box, Arc)]
 pub trait GasOracle: Send + Sync + std::fmt::Debug {
     /// Makes an asynchronous HTTP query to the underlying `GasOracle`
     ///
diff --git a/ethers-middleware/src/gas_oracle/provider_oracle.rs b/ethers-middleware/src/gas_oracle/provider_oracle.rs
new file mode 100644
index 00000000..894bb5ab
--- /dev/null
+++ b/ethers-middleware/src/gas_oracle/provider_oracle.rs
@@ -0,0 +1,40 @@
+use crate::gas_oracle::{GasOracle, GasOracleError};
+use async_trait::async_trait;
+use ethers_core::types::U256;
+use ethers_providers::Middleware;
+use std::fmt::Debug;
+
+/// Gas oracle from a [`Middleware`] implementation such as an
+/// Ethereum RPC provider.
+#[derive(Debug)]
+pub struct ProviderOracle<M: Middleware> {
+    provider: M,
+}
+
+impl<M: Middleware> ProviderOracle<M> {
+    pub fn new(provider: M) -> Self {
+        Self { provider }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl<M: Middleware> GasOracle for ProviderOracle<M>
+where
+    <M as Middleware>::Error: 'static,
+{
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        self.provider
+            .get_gas_price()
+            .await
+            .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
+    }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        // TODO: Allow configuring different estimation functions.
+        self.provider
+            .estimate_eip1559_fees(None)
+            .await
+            .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
+    }
+}
diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs
index e38ed7e1..b911a660 100644
--- a/ethers-middleware/src/signer.rs
+++ b/ethers-middleware/src/signer.rs
@@ -303,9 +303,13 @@ where
         self.signer.sign_message(data.into()).await.map_err(SignerMiddlewareError::SignerError)
     }
 
-    async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
+    async fn estimate_gas(
+        &self,
+        tx: &TypedTransaction,
+        block: Option<BlockId>,
+    ) -> Result<U256, Self::Error> {
         let tx = self.set_tx_from_if_none(tx);
-        self.inner.estimate_gas(&tx).await.map_err(SignerMiddlewareError::MiddlewareError)
+        self.inner.estimate_gas(&tx, block).await.map_err(SignerMiddlewareError::MiddlewareError)
     }
 
     async fn create_access_list(
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index 9e7ee807..8a323db1 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -20,7 +20,7 @@ mod pending_escalator;
 pub use pending_escalator::EscalatingPending;
 
 mod log_query;
-pub use log_query::LogQuery;
+pub use log_query::{LogQuery, LogQueryError};
 
 mod stream;
 pub use futures_util::StreamExt;
@@ -88,15 +88,6 @@ where
     }
 }
 
-/// Structure used in eth_syncing RPC
-#[derive(Clone, Debug)]
-pub enum SyncingStatus {
-    /// When client is synced to highest block, eth_syncing with return string "false"
-    IsFalse,
-    /// When client is still syncing past blocks we get IsSyncing information.
-    IsSyncing { starting_block: U256, current_block: U256, highest_block: U256 },
-}
-
 /// A middleware allows customizing requests send and received from an ethereum node.
 ///
 /// Writing a middleware is as simple as:
@@ -106,7 +97,7 @@ pub enum SyncingStatus {
 ///
 /// ```rust
 /// use ethers_providers::{Middleware, FromErr};
-/// use ethers_core::types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction};
+/// use ethers_core::types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction, BlockId};
 /// use thiserror::Error;
 /// use async_trait::async_trait;
 ///
@@ -147,9 +138,9 @@ pub enum SyncingStatus {
 ///
 ///     /// Overrides the default `estimate_gas` method to log that it was called,
 ///     /// before forwarding the call to the next layer.
-///     async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
+///     async fn estimate_gas(&self, tx: &TypedTransaction, block: Option<BlockId>) -> Result<U256, Self::Error> {
 ///         println!("Estimating gas...");
-///         self.inner().estimate_gas(tx).await.map_err(FromErr::from)
+///         self.inner().estimate_gas(tx, block).await.map_err(FromErr::from)
 ///     }
 /// }
 /// ```
@@ -182,15 +173,11 @@ pub trait Middleware: Sync + Send + Debug {
     /// This function is defined on providers to behave as follows:
     /// 1. populate the `from` field with the default sender
     /// 2. resolve any ENS names in the tx `to` field
-    /// 3. Estimate gas usage _without_ access lists
-    /// 4. Estimate gas usage _with_ access lists
-    /// 5. Enable access lists IFF they are cheaper
-    /// 6. Poll and set legacy or 1559 gas prices
-    /// 7. Set the chain_id with the provider's, if not already set
+    /// 3. Estimate gas usage
+    /// 4. Poll and set legacy or 1559 gas prices
+    /// 5. Set the chain_id with the provider's, if not already set
     ///
     /// It does NOT set the nonce by default.
-    /// It MAY override the gas amount set by the user, if access lists are
-    /// cheaper.
     ///
     /// Middleware are encouraged to override any values _before_ delegating
     /// to the inner implementation AND/OR modify the values provided by the
@@ -321,8 +308,12 @@ pub trait Middleware: Sync + Send + Debug {
         self.inner().get_transaction_count(from, block).await.map_err(FromErr::from)
     }
 
-    async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
-        self.inner().estimate_gas(tx).await.map_err(FromErr::from)
+    async fn estimate_gas(
+        &self,
+        tx: &TypedTransaction,
+        block: Option<BlockId>,
+    ) -> Result<U256, Self::Error> {
+        self.inner().estimate_gas(tx, block).await.map_err(FromErr::from)
     }
 
     async fn call(
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index d4d0a0eb..7d36041b 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -29,7 +29,7 @@ use ethers_core::{
     utils,
 };
 use hex::FromHex;
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde::{de::DeserializeOwned, Serialize};
 use thiserror::Error;
 use url::{ParseError, Url};
 
@@ -352,34 +352,10 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
             }
         }
 
-        // If the tx has an access list but it is empty, it is an Eip1559 or Eip2930 tx,
-        // and we attempt to populate the acccess list. This may require `eth_estimateGas`,
-        // in which case we save the result in maybe_gas_res for later
-        let mut maybe_gas = None;
-        if let Some(starting_al) = tx.access_list() {
-            if starting_al.0.is_empty() {
-                let (gas_res, al_res) = futures_util::join!(
-                    maybe(tx.gas().cloned(), self.estimate_gas(tx)),
-                    self.create_access_list(tx, block)
-                );
-                let mut gas = gas_res?;
-
-                if let Ok(al_with_gas) = al_res {
-                    // Set access list if it saves gas over the estimated (or previously set) value
-                    if al_with_gas.gas_used < gas {
-                        // Update the gas estimate with the lower amount
-                        gas = al_with_gas.gas_used;
-                        tx.set_access_list(al_with_gas.access_list);
-                    }
-                }
-                maybe_gas = Some(gas);
-            }
-        }
-
         // Set gas to estimated value only if it was not set by the caller,
         // even if the access list has been populated and saves gas
         if tx.gas().is_none() {
-            let gas_estimate = maybe(maybe_gas, self.estimate_gas(tx)).await?;
+            let gas_estimate = self.estimate_gas(tx, block).await?;
             tx.set_gas(gas_estimate);
         }
 
@@ -563,26 +539,7 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
 
     /// Return current client syncing status. If IsFalse sync is over.
     async fn syncing(&self) -> Result<SyncingStatus, Self::Error> {
-        #[derive(Debug, Serialize, Deserialize)]
-        #[serde(untagged)]
-        pub enum SyncingStatusIntermediate {
-            /// When client is synced to highest block, eth_syncing with return string "false"
-            IsFalse(bool),
-            /// When client is still syncing past blocks we get IsSyncing information.
-            IsSyncing { starting_block: U256, current_block: U256, highest_block: U256 },
-        }
-        let intermediate: SyncingStatusIntermediate = self.request("eth_syncing", ()).await?;
-        match intermediate {
-            SyncingStatusIntermediate::IsFalse(false) => Ok(SyncingStatus::IsFalse),
-            SyncingStatusIntermediate::IsFalse(true) => Err(ProviderError::CustomError(
-                "eth_syncing returned `true` that is undefined value.".to_owned(),
-            )),
-            SyncingStatusIntermediate::IsSyncing {
-                starting_block,
-                current_block,
-                highest_block,
-            } => Ok(SyncingStatus::IsSyncing { starting_block, current_block, highest_block }),
-        }
+        self.request("eth_syncing", ()).await
     }
 
     /// Returns the network version.
@@ -611,8 +568,14 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
     /// required (as a U256) to send it This is free, but only an estimate. Providing too little
     /// gas will result in a transaction being rejected (while still consuming all provided
     /// gas).
-    async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, ProviderError> {
-        self.request("eth_estimateGas", [tx]).await
+    async fn estimate_gas(
+        &self,
+        tx: &TypedTransaction,
+        block: Option<BlockId>,
+    ) -> Result<U256, ProviderError> {
+        let tx = utils::serialize(tx);
+        let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
+        self.request("eth_estimateGas", [tx, block]).await
     }
 
     async fn create_access_list(
@@ -2064,42 +2027,14 @@ mod tests {
         assert_eq!(tx.gas_price(), Some(max_fee));
         assert_eq!(tx.access_list(), Some(&access_list));
 
-        // --- fills a 1559 transaction, leaving the existing gas limit unchanged, but including
-        // access list if cheaper
-        let gas_with_al = gas - 1;
+        // --- fills a 1559 transaction, leaving the existing gas limit unchanged,
+        // without generating an access-list
         let mut tx = Eip1559TransactionRequest::new()
             .gas(gas)
             .max_fee_per_gas(max_fee)
             .max_priority_fee_per_gas(prio_fee)
             .into();
 
-        mock.push(AccessListWithGasUsed {
-            access_list: access_list.clone(),
-            gas_used: gas_with_al,
-        })
-        .unwrap();
-
-        provider.fill_transaction(&mut tx, None).await.unwrap();
-
-        assert_eq!(tx.from(), provider.from.as_ref());
-        assert!(tx.to().is_none());
-        assert_eq!(tx.gas(), Some(&gas));
-        assert_eq!(tx.access_list(), Some(&access_list));
-
-        // --- fills a 1559 transaction, ignoring access list if more expensive
-        let gas_with_al = gas + 1;
-        let mut tx = Eip1559TransactionRequest::new()
-            .max_fee_per_gas(max_fee)
-            .max_priority_fee_per_gas(prio_fee)
-            .into();
-
-        mock.push(AccessListWithGasUsed {
-            access_list: access_list.clone(),
-            gas_used: gas_with_al,
-        })
-        .unwrap();
-        mock.push(gas).unwrap();
-
         provider.fill_transaction(&mut tx, None).await.unwrap();
 
         assert_eq!(tx.from(), provider.from.as_ref());
@@ -2107,14 +2042,12 @@ mod tests {
         assert_eq!(tx.gas(), Some(&gas));
         assert_eq!(tx.access_list(), Some(&Default::default()));
 
-        // --- fills a 1559 transaction, using estimated gas if create_access_list() errors
+        // --- fills a 1559 transaction, using estimated gas
         let mut tx = Eip1559TransactionRequest::new()
             .max_fee_per_gas(max_fee)
             .max_priority_fee_per_gas(prio_fee)
             .into();
 
-        // bad mock value causes error response for eth_createAccessList
-        mock.push(b'b').unwrap();
         mock.push(gas).unwrap();
 
         provider.fill_transaction(&mut tx, None).await.unwrap();
diff --git a/ethers-providers/src/pubsub.rs b/ethers-providers/src/pubsub.rs
index 63339437..a31c0dcb 100644
--- a/ethers-providers/src/pubsub.rs
+++ b/ethers-providers/src/pubsub.rs
@@ -12,6 +12,7 @@ use std::{
     pin::Pin,
     task::{Context, Poll},
 };
+use tracing::error;
 
 /// A transport implementation supporting pub sub subscriptions.
 pub trait PubsubClient: JsonRpcClient {
@@ -90,7 +91,10 @@ where
         match futures_util::ready!(this.rx.poll_next(ctx)) {
             Some(item) => match serde_json::from_str(item.get()) {
                 Ok(res) => Poll::Ready(Some(res)),
-                _ => Poll::Pending,
+                Err(err) => {
+                    error!("failed to deserialize item {:?}", err);
+                    Poll::Pending
+                }
             },
             None => Poll::Ready(None),
         }
diff --git a/ethers-providers/src/transports/http.rs b/ethers-providers/src/transports/http.rs
index 4753cc2b..976aab64 100644
--- a/ethers-providers/src/transports/http.rs
+++ b/ethers-providers/src/transports/http.rs
@@ -52,7 +52,10 @@ pub enum ClientError {
 
 impl From<ClientError> for ProviderError {
     fn from(src: ClientError) -> Self {
-        ProviderError::JsonRpcClientError(Box::new(src))
+        match src {
+            ClientError::ReqwestError(err) => ProviderError::HTTPError(err),
+            _ => ProviderError::JsonRpcClientError(Box::new(src)),
+        }
     }
 }
 
diff --git a/ethers-providers/src/transports/mock.rs b/ethers-providers/src/transports/mock.rs
index 8d387552..2c129b61 100644
--- a/ethers-providers/src/transports/mock.rs
+++ b/ethers-providers/src/transports/mock.rs
@@ -10,10 +10,19 @@ use std::{
 };
 use thiserror::Error;
 
+/// Helper type that can be used to pass through the `params` value.
+/// This is necessary because the wrapper provider is supposed to skip the `params` if it's of
+/// size 0, see `crate::transports::common::Request`
+#[derive(Debug)]
+enum MockParams {
+    Value(Value),
+    Zst
+}
+
 #[derive(Clone, Debug)]
 /// Mock transport used in test environments.
 pub struct MockProvider {
-    requests: Arc<Mutex<VecDeque<(String, Value)>>>,
+    requests: Arc<Mutex<VecDeque<(String, MockParams)>>>,
     responses: Arc<Mutex<VecDeque<Value>>>,
 }
 
@@ -28,14 +37,19 @@ impl Default for MockProvider {
 impl JsonRpcClient for MockProvider {
     type Error = MockError;
 
-    /// Pushes the `(method, input)` to the back of the `requests` queue,
+    /// Pushes the `(method, params)` to the back of the `requests` queue,
     /// pops the responses from the back of the `responses` queue
     async fn request<T: Serialize + Send + Sync, R: DeserializeOwned>(
         &self,
         method: &str,
-        input: T,
+        params: T,
     ) -> Result<R, MockError> {
-        self.requests.lock().unwrap().push_back((method.to_owned(), serde_json::to_value(input)?));
+        let params = if std::mem::size_of::<T>() == 0 {
+            MockParams::Zst
+        } else {
+            MockParams::Value(serde_json::to_value(params)?)
+        };
+        self.requests.lock().unwrap().push_back((method.to_owned(), params));
         let mut data = self.responses.lock().unwrap();
         let element = data.pop_back().ok_or(MockError::EmptyResponses)?;
         let res: R = serde_json::from_value(element)?;
@@ -53,7 +67,15 @@ impl MockProvider {
     ) -> Result<(), MockError> {
         let (m, inp) = self.requests.lock().unwrap().pop_front().ok_or(MockError::EmptyRequests)?;
         assert_eq!(m, method);
-        assert_eq!(serde_json::to_value(data).expect("could not serialize data"), inp);
+        assert!(!matches!(inp, MockParams::Value(serde_json::Value::Null)));
+        if std::mem::size_of::<T>() == 0 {
+            assert!(matches!(inp, MockParams::Zst));
+        } else if let MockParams::Value(inp) = inp {
+            assert_eq!(serde_json::to_value(data).expect("could not serialize data"), inp);
+        } else {
+            unreachable!("Zero sized types must be denoted with MockParams::Zst")
+        }
+
         Ok(())
     }
 
diff --git a/ethers-providers/src/transports/quorum.rs b/ethers-providers/src/transports/quorum.rs
index 976d29c8..918d3058 100644
--- a/ethers-providers/src/transports/quorum.rs
+++ b/ethers-providers/src/transports/quorum.rs
@@ -1,5 +1,4 @@
 use std::{
-    fmt,
     fmt::Debug,
     future::Future,
     pin::Pin,
@@ -166,7 +165,7 @@ impl<T: JsonRpcClientWrapper> QuorumProvider<T> {
     /// This is the minimum of all provider's block numbers
     async fn get_minimum_block_number(&self) -> Result<U64, ProviderError> {
         let mut numbers = join_all(self.providers.iter().map(|provider| async move {
-            let block = provider.inner.request("eth_blockNumber", serde_json::json!(())).await?;
+            let block = provider.inner.request("eth_blockNumber", QuorumParams::Zst).await?;
             serde_json::from_value::<U64>(block).map_err(ProviderError::from)
         }))
         .await
@@ -181,7 +180,13 @@ impl<T: JsonRpcClientWrapper> QuorumProvider<T> {
     }
 
     /// Normalizes the request payload depending on the call
-    async fn normalize_request(&self, method: &str, params: &mut Value) {
+    async fn normalize_request(&self, method: &str, q_params: &mut QuorumParams) {
+        let params = if let QuorumParams::Value(v) = q_params {
+            v
+        } else {
+            // at this time no normalization is required for calls with zero parameters.
+            return
+        };
         match method {
             "eth_call" |
             "eth_createAccessList" |
@@ -364,8 +369,8 @@ impl From<QuorumError> for ProviderError {
 
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-pub trait JsonRpcClientWrapper: Send + Sync + fmt::Debug {
-    async fn request(&self, method: &str, params: Value) -> Result<Value, ProviderError>;
+pub trait JsonRpcClientWrapper: Send + Sync + Debug {
+    async fn request(&self, method: &str, params: QuorumParams) -> Result<Value, ProviderError>;
 }
 type NotificationStream =
     Box<dyn futures_core::Stream<Item = Box<RawValue>> + Send + Unpin + 'static>;
@@ -381,14 +386,20 @@ pub trait PubsubClientWrapper: JsonRpcClientWrapper {
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl<C: JsonRpcClient> JsonRpcClientWrapper for C {
-    async fn request(&self, method: &str, params: Value) -> Result<Value, ProviderError> {
-        Ok(JsonRpcClient::request(self, method, params).await.map_err(C::Error::into)?)
+    async fn request(&self, method: &str, params: QuorumParams) -> Result<Value, ProviderError> {
+        let fut = if let QuorumParams::Value(params) = params {
+            JsonRpcClient::request(self, method, params)
+        } else {
+            JsonRpcClient::request(self, method, ())
+        };
+
+        Ok(fut.await.map_err(C::Error::into)?)
     }
 }
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl JsonRpcClientWrapper for Box<dyn JsonRpcClientWrapper> {
-    async fn request(&self, method: &str, params: Value) -> Result<Value, ProviderError> {
+    async fn request(&self, method: &str, params: QuorumParams) -> Result<Value, ProviderError> {
         self.as_ref().request(method, params).await
     }
 }
@@ -396,7 +407,7 @@ impl JsonRpcClientWrapper for Box<dyn JsonRpcClientWrapper> {
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl JsonRpcClientWrapper for Box<dyn PubsubClientWrapper> {
-    async fn request(&self, method: &str, params: Value) -> Result<Value, ProviderError> {
+    async fn request(&self, method: &str, params: QuorumParams) -> Result<Value, ProviderError> {
         self.as_ref().request(method, params).await
     }
 }
@@ -437,7 +448,12 @@ where
         method: &str,
         params: T,
     ) -> Result<R, Self::Error> {
-        let mut params = serde_json::to_value(params)?;
+        let mut params = if std::mem::size_of::<T>() == 0 {
+            // we don't want `()` to become `"null"`.
+            QuorumParams::Zst
+        } else {
+            QuorumParams::Value(serde_json::to_value(params)?)
+        };
         self.normalize_request(method, &mut params).await;
 
         let requests = self
@@ -556,6 +572,15 @@ where
     }
 }
 
+/// Helper type that can be used to pass through the `params` value.
+/// This is necessary because the wrapper provider is supposed to skip the `params` if it's of
+/// size 0, see `crate::transports::common::Request`
+#[derive(Clone)]
+pub enum QuorumParams {
+    Value(Value),
+    Zst
+}
+
 #[cfg(test)]
 #[cfg(not(target_arch = "wasm32"))]
 mod tests {
diff --git a/ethers-providers/src/transports/retry.rs b/ethers-providers/src/transports/retry.rs
index 8cba35f4..41495e56 100644
--- a/ethers-providers/src/transports/retry.rs
+++ b/ethers-providers/src/transports/retry.rs
@@ -22,6 +22,27 @@ pub trait RetryPolicy<E>: Send + Sync + Debug {
 
 /// [RetryClient] presents as a wrapper around [JsonRpcClient] that will retry
 /// requests based with an exponential backoff and filtering based on [RetryPolicy].
+///
+/// The `RetryPolicy`, mainly for rate-limiting errors, can be adjusted for specific applications,
+/// endpoints. In addition to the `RetryPolicy` errors due to connectivity issues, like timed out
+/// connections or responses in range `5xx` can be retried separately.
+///
+/// # Example
+///
+/// ```
+/// #  async fn demo() {
+/// use ethers_providers::{Http, RetryClient, RetryClientBuilder, HttpRateLimitRetryPolicy};
+/// use std::time::Duration;
+/// use url::Url;
+///
+/// let http = Http::new(Url::parse("http://localhost:8545").unwrap());
+/// let client = RetryClientBuilder::default()
+///     .rate_limit_retries(10)
+///     .timeout_retries(3)
+///     .initial_backoff(Duration::from_millis(500))
+///     .build(http, Box::new(HttpRateLimitRetryPolicy::default()));
+/// # }
+/// ```
 #[derive(Debug)]
 pub struct RetryClient<T>
 where
@@ -30,9 +51,14 @@ where
 {
     inner: T,
     requests_enqueued: AtomicU32,
+    /// The policy to use to determine whether to retry a request due to rate limiting
     policy: Box<dyn RetryPolicy<T::Error>>,
-    max_retry: u32,
-    initial_backoff: u64,
+    /// How many connection `TimedOut` should be retried.
+    timeout_retries: u32,
+    /// How many retries for rate limited responses
+    rate_limit_retries: u32,
+    /// How long to wait initially
+    initial_backoff: Duration,
     /// available CPU per second
     compute_units_per_second: u64,
 }
@@ -67,15 +93,10 @@ where
         // in milliseconds
         initial_backoff: u64,
     ) -> Self {
-        Self {
-            inner,
-            requests_enqueued: AtomicU32::new(0),
-            policy,
-            max_retry,
-            initial_backoff,
-            // alchemy max cpus <https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups>
-            compute_units_per_second: 330,
-        }
+        RetryClientBuilder::default()
+            .initial_backoff(Duration::from_millis(initial_backoff))
+            .rate_limit_retries(max_retry)
+            .build(inner, policy)
     }
 
     /// Sets the free compute units per second limit.
@@ -90,40 +111,113 @@ where
     }
 }
 
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct RetryClientBuilder {
+    /// How many connection `TimedOut` should be retried.
+    timeout_retries: u32,
+    /// How many retries for rate limited responses
+    rate_limit_retries: u32,
+    /// How long to wait initially
+    initial_backoff: Duration,
+    /// available CPU per second
+    compute_units_per_second: u64,
+}
+
+// === impl RetryClientBuilder ===
+
+impl RetryClientBuilder {
+    /// Sets the number of retries after a connection times out
+    ///
+    /// **Note:** this will only be used for `request::Error::TimedOut`
+    pub fn timeout_retries(mut self, timeout_retries: u32) -> Self {
+        self.timeout_retries = timeout_retries;
+        self
+    }
+
+    /// How many retries for rate limited responses
+    pub fn rate_limit_retries(mut self, rate_limit_retries: u32) -> Self {
+        self.rate_limit_retries = rate_limit_retries;
+        self
+    }
+
+    /// Sets the number of assumed available compute units per second
+    ///
+    /// See also, <https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups>
+    pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
+        self.compute_units_per_second = compute_units_per_second;
+        self
+    }
+
+    /// Sets the duration to wait initially before retrying
+    pub fn initial_backoff(mut self, initial_backoff: Duration) -> Self {
+        self.initial_backoff = initial_backoff;
+        self
+    }
+
+    /// Creates the `RetryClient` with the configured settings
+    pub fn build<T>(self, client: T, policy: Box<dyn RetryPolicy<T::Error>>) -> RetryClient<T>
+    where
+        T: JsonRpcClient,
+        T::Error: Sync + Send + 'static,
+    {
+        let RetryClientBuilder {
+            timeout_retries,
+            rate_limit_retries,
+            initial_backoff,
+            compute_units_per_second,
+        } = self;
+        RetryClient {
+            inner: client,
+            requests_enqueued: AtomicU32::new(0),
+            policy,
+            timeout_retries,
+            rate_limit_retries,
+            initial_backoff,
+            compute_units_per_second,
+        }
+    }
+}
+
+// Some sensible defaults
+impl Default for RetryClientBuilder {
+    fn default() -> Self {
+        Self {
+            timeout_retries: 3,
+            // this should be enough to even out heavy loads
+            rate_limit_retries: 10,
+            initial_backoff: Duration::from_millis(100),
+            // alchemy max cpus <https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups>
+            compute_units_per_second: 330,
+        }
+    }
+}
+
 /// 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,
-{
+pub enum RetryClientError {
     #[error(transparent)]
-    ProviderError(T::Error),
+    ProviderError(ProviderError),
     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,
-{
+impl std::fmt::Display for RetryClientError {
     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))
+impl From<RetryClientError> for ProviderError {
+    fn from(src: RetryClientError) -> Self {
+        match src {
+            RetryClientError::ProviderError(err) => err,
+            RetryClientError::TimeoutError => ProviderError::JsonRpcClientError(Box::new(src)),
+            RetryClientError::SerdeJson(err) => err.into(),
+        }
     }
 }
 
@@ -133,11 +227,11 @@ where
     T: JsonRpcClient + 'static,
     T::Error: Sync + Send + 'static,
 {
-    type Error = RetryClientError<T>;
+    type Error = RetryClientError;
 
     async fn request<A, R>(&self, method: &str, params: A) -> Result<R, Self::Error>
     where
-        A: std::fmt::Debug + Serialize + Send + Sync,
+        A: Debug + Serialize + Send + Sync,
         R: DeserializeOwned,
     {
         // Helper type that caches the `params` value across several retries
@@ -151,14 +245,14 @@ where
         let params = if std::mem::size_of::<A>() == 0 {
             RetryParams::Zst(())
         } else {
-            let params =
-                serde_json::to_value(params).map_err(|err| RetryClientError::SerdeJson(err))?;
+            let params = serde_json::to_value(params).map_err(RetryClientError::SerdeJson)?;
             RetryParams::Value(params)
         };
 
         let ahead_in_queue = self.requests_enqueued.fetch_add(1, Ordering::SeqCst) as u64;
 
-        let mut retry_number: u32 = 0;
+        let mut rate_limit_retry_number: u32 = 0;
+        let mut timeout_retries: u32 = 0;
 
         loop {
             let err;
@@ -179,20 +273,22 @@ where
                 }
             }
 
-            retry_number += 1;
-            if retry_number > self.max_retry {
-                trace!("request timed out after {} retries", self.max_retry);
-                return Err(RetryClientError::TimeoutError)
-            }
-
             let should_retry = self.policy.should_retry(&err);
             if should_retry {
+                rate_limit_retry_number += 1;
+                if rate_limit_retry_number > self.rate_limit_retries {
+                    trace!("request timed out after {} retries", self.rate_limit_retries);
+                    return Err(RetryClientError::TimeoutError)
+                }
+
                 let current_queued_requests = self.requests_enqueued.load(Ordering::SeqCst) as u64;
                 // using `retry_number` for creating back pressure because
                 // of already queued requests
                 // this increases exponentially with retries and adds a delay based on how many
                 // requests are currently queued
-                let mut next_backoff = self.initial_backoff * 2u64.pow(retry_number);
+                let mut next_backoff = Duration::from_millis(
+                    self.initial_backoff.as_millis().pow(rate_limit_retry_number) as u64,
+                );
 
                 // requests are usually weighted and can vary from 10 CU to several 100 CU, cheaper
                 // requests are more common some example alchemy weights:
@@ -209,12 +305,18 @@ where
                     current_queued_requests,
                     ahead_in_queue,
                 );
-                // backoff is measured in millis
-                next_backoff += seconds_to_wait_for_compute_budge * 1000;
+                next_backoff += Duration::from_secs(seconds_to_wait_for_compute_budge);
 
-                trace!("retrying and backing off for {}ms", next_backoff);
-                tokio::time::sleep(Duration::from_millis(next_backoff)).await;
+                trace!("retrying and backing off for {:?}", next_backoff);
+                tokio::time::sleep(next_backoff).await;
             } else {
+                let err: ProviderError = err.into();
+                if timeout_retries < self.timeout_retries && maybe_connectivity(&err) {
+                    timeout_retries += 1;
+                    trace!(err = ?err, "retrying due to spurious network");
+                    continue
+                }
+
                 trace!(err = ?err, "should not retry");
                 self.requests_enqueued.fetch_sub(1, Ordering::SeqCst);
                 return Err(RetryClientError::ProviderError(err))
@@ -266,6 +368,24 @@ fn compute_unit_offset_in_secs(
     }
 }
 
+/// Checks whether the `error` is the result of a connectivity issue, like
+/// `request::Error::TimedOut`
+fn maybe_connectivity(err: &ProviderError) -> bool {
+    if let ProviderError::HTTPError(reqwest_err) = err {
+        if reqwest_err.is_timeout() || reqwest_err.is_connect() {
+            return true
+        }
+        // Error HTTP codes (5xx) are considered connectivity issues and will prompt retry
+        if let Some(status) = reqwest_err.status() {
+            let code = status.as_u16();
+            if (500..600).contains(&code) {
+                return true
+            }
+        }
+    }
+    false
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs
index 098a2d87..e43d5ed8 100644
--- a/ethers-providers/src/transports/ws.rs
+++ b/ethers-providers/src/transports/ws.rs
@@ -22,6 +22,7 @@ use std::{
     },
 };
 use thiserror::Error;
+use tracing::trace;
 
 use super::common::{Params, Response};
 
@@ -317,6 +318,7 @@ where
     }
 
     async fn handle_text(&mut self, inner: String) -> Result<(), ClientError> {
+        trace!(msg=?inner, "received message");
         let (id, result) = match serde_json::from_str(&inner)? {
             Response::Success { id, result } => (id, Ok(result.to_owned())),
             Response::Error { id, error } => (id, Err(error)),
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index c3282baa..b8bd4b31 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -43,8 +43,8 @@ cfg-if = "1.0.0"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 home = "0.5.3"
-svm = { package = "svm-rs", version = "0.2.15", default-features = false, optional = true, features = ["blocking"] }
-svm-builds = { package = "svm-rs-builds", version = "0.1.7", optional = true}
+svm = { package = "svm-rs", version = "0.2.16", default-features = false, optional = true, features = ["blocking"] }
+svm-builds = { package = "svm-rs-builds", version = "0.1.8", optional = true}
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 # NOTE: this enables wasm compatibility for getrandom indirectly
diff --git a/ethers-solc/src/artifact_output/files.rs b/ethers-solc/src/artifact_output/files.rs
new file mode 100644
index 00000000..12c88ad4
--- /dev/null
+++ b/ethers-solc/src/artifact_output/files.rs
@@ -0,0 +1,60 @@
+use crate::contracts::VersionedContract;
+use std::{
+    collections::HashMap,
+    hash::Hash,
+    ops::{Deref, DerefMut},
+    path::{Path, PathBuf},
+};
+
+/// Container type for all contracts mapped to their output file
+pub struct MappedArtifactFiles<'a> {
+    /// Represents the determined output artifact file and the contract(s) that target it
+    ///
+    /// This is guaranteed to be `len >= 1`
+    ///
+    /// If there is more than 1 contract in the map, this means we have a naming conflict where
+    /// different contracts target the same output file. This happens if the solidity file and
+    /// contract name match, but they are in different folders.
+    pub files: HashMap<MappedArtifactFile, Vec<MappedContract<'a>>>,
+}
+
+impl<'a> MappedArtifactFiles<'a> {
+    pub fn with_capacity(len: usize) -> Self {
+        Self { files: HashMap::with_capacity(len) }
+    }
+}
+
+impl<'a> Deref for MappedArtifactFiles<'a> {
+    type Target = HashMap<MappedArtifactFile, Vec<MappedContract<'a>>>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.files
+    }
+}
+
+impl<'a> DerefMut for MappedArtifactFiles<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.files
+    }
+}
+
+/// Represents the targeted path of a contract or multiple contracts
+///
+/// To account for case-sensitivity we identify it via lowercase path
+#[derive(Debug, Hash, PartialEq, Eq)]
+pub struct MappedArtifactFile {
+    lower_case_path: String,
+}
+
+impl MappedArtifactFile {
+    pub fn new(path: &Path) -> Self {
+        Self { lower_case_path: path.to_string_lossy().to_lowercase() }
+    }
+}
+
+pub struct MappedContract<'a> {
+    pub file: &'a str,
+    pub name: &'a str,
+    pub contract: &'a VersionedContract,
+    pub artifact_path: PathBuf,
+}
diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs
index 11759fae..37528dc1 100644
--- a/ethers-solc/src/artifact_output/mod.rs
+++ b/ethers-solc/src/artifact_output/mod.rs
@@ -10,23 +10,27 @@ use crate::{
     error::Result,
     sourcemap::{SourceMap, SyntaxError},
     sources::VersionedSourceFile,
-    utils, HardhatArtifact, ProjectPathsConfig, SolcError,
+    utils, HardhatArtifact, ProjectPathsConfig, SolFilesCache, SolcError,
 };
 use ethers_core::{abi::Abi, types::Bytes};
 use semver::Version;
 use serde::{de::DeserializeOwned, Serialize};
 use std::{
     borrow::Cow,
-    collections::{btree_map::BTreeMap, HashMap, HashSet},
+    collections::{btree_map::BTreeMap, HashSet},
     ffi::OsString,
-    fmt, fs, io,
+    fmt, fs,
+    hash::Hash,
+    io,
     ops::Deref,
     path::{Path, PathBuf},
 };
-use tracing::trace;
+use tracing::{error, trace};
 
 mod configurable;
-use crate::contracts::VersionedContract;
+pub(crate) mod files;
+
+use crate::files::MappedContract;
 pub use configurable::*;
 
 /// Represents unique artifact metadata for identifying artifacts on output
@@ -98,6 +102,7 @@ pub struct ArtifactFile<T> {
 impl<T: Serialize> ArtifactFile<T> {
     /// Writes the given contract to the `out` path creating all parent directories
     pub fn write(&self) -> Result<()> {
+        trace!("writing artifact file {:?} {}", self.file, self.version);
         utils::create_parent_dir_all(&self.file)?;
         fs::write(&self.file, serde_json::to_vec_pretty(&self.artifact)?)
             .map_err(|err| SolcError::io(err, &self.file))?;
@@ -119,11 +124,6 @@ impl<T> ArtifactFile<T> {
     }
 }
 
-/// Internal helper type alias that maps all files for the contracts
-/// `output -> [(file, name, contract)]`
-pub(crate) type ArtifactFiles<'a> =
-    HashMap<PathBuf, Vec<(&'a str, &'a str, &'a VersionedContract)>>;
-
 /// local helper type alias `file name -> (contract name  -> Vec<..>)`
 pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
 
@@ -574,8 +574,9 @@ pub trait ArtifactOutput {
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
         layout: &ProjectPathsConfig,
+        ctx: OutputContext,
     ) -> Result<Artifacts<Self::Artifact>> {
-        let mut artifacts = self.output_to_artifacts(contracts, sources);
+        let mut artifacts = self.output_to_artifacts(contracts, sources, ctx);
         artifacts.join_all(&layout.artifacts);
         artifacts.write_all()?;
 
@@ -792,6 +793,7 @@ pub trait ArtifactOutput {
         &self,
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
+        ctx: OutputContext,
     ) -> Artifacts<Self::Artifact> {
         let mut artifacts = ArtifactsMap::new();
 
@@ -799,14 +801,15 @@ pub trait ArtifactOutput {
         let mut non_standalone_sources = HashSet::new();
 
         // this holds all output files and the contract(s) it belongs to
-        let artifact_files = contracts.artifact_files::<Self>();
+        let artifact_files = contracts.artifact_files::<Self>(&ctx);
 
         // this tracks the final artifacts, which we use as lookup for checking conflicts when
         // converting stand-alone artifacts in the next step
         let mut final_artifact_paths = HashSet::new();
 
-        for (artifact_path, contracts) in artifact_files {
-            for (idx, (file, name, contract)) in contracts.iter().enumerate() {
+        for contracts in artifact_files.files.into_values() {
+            for (idx, mapped_contract) in contracts.iter().enumerate() {
+                let MappedContract { file, name, contract, artifact_path } = mapped_contract;
                 // track `SourceFile`s that can be mapped to contracts
                 let source_file = sources.find_file_and_version(file, &contract.version);
 
@@ -821,11 +824,11 @@ pub trait ArtifactOutput {
                     // contracts need to adjust the paths properly
 
                     // we keep the top most conflicting file unchanged
-                    let is_top_most = contracts.iter().enumerate().filter(|(i, _)| *i != idx).all(
-                        |(_, (f, _, _))| {
-                            Path::new(file).components().count() < Path::new(f).components().count()
-                        },
-                    );
+                    let is_top_most =
+                        contracts.iter().enumerate().filter(|(i, _)| *i != idx).all(|(_, c)| {
+                            Path::new(file).components().count() <
+                                Path::new(c.file).components().count()
+                        });
                     if !is_top_most {
                         // we resolve the conflicting by finding a new unique, alternative path
                         artifact_path = Self::conflict_free_output_file(
@@ -932,6 +935,46 @@ pub trait ArtifactOutput {
     ) -> Option<Self::Artifact>;
 }
 
+/// Additional context to use during [`ArtifactOutput::on_output()`]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
+pub struct OutputContext<'a> {
+    /// Cache file of the project or empty if no caching is enabled
+    ///
+    /// This context is required for partially cached recompile with conflicting files, so that we
+    /// can use the same adjusted output path for conflicting files like:
+    ///
+    /// ```text
+    /// src
+    /// ├── a.sol
+    /// └── inner
+    ///     └── a.sol
+    /// ```
+    pub cache: Cow<'a, SolFilesCache>,
+}
+
+// === impl OutputContext
+
+impl<'a> OutputContext<'a> {
+    /// Create a new context with the given cache file
+    pub fn new(cache: &'a SolFilesCache) -> Self {
+        Self { cache: Cow::Borrowed(cache) }
+    }
+
+    /// Returns the path of the already existing artifact for the `contract` of the `file` compiled
+    /// with the `version`.
+    ///
+    /// Returns `None` if no file exists
+    pub fn existing_artifact(
+        &self,
+        file: impl AsRef<Path>,
+        contract: &str,
+        version: &Version,
+    ) -> Option<&PathBuf> {
+        self.cache.entry(file)?.find_artifact(contract, version)
+    }
+}
+
 /// An `Artifact` implementation that uses a compact representation
 ///
 /// Creates a single json artifact with
@@ -984,8 +1027,9 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
         output: &VersionedContracts,
         sources: &VersionedSourceFiles,
         layout: &ProjectPathsConfig,
+        ctx: OutputContext,
     ) -> Result<Artifacts<Self::Artifact>> {
-        MinimalCombinedArtifacts::default().on_output(output, sources, layout)
+        MinimalCombinedArtifacts::default().on_output(output, sources, layout, ctx)
     }
 
     fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
@@ -994,10 +1038,10 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
         if let Ok(a) = serde_json::from_str(&content) {
             Ok(a)
         } else {
-            tracing::error!("Failed to deserialize compact artifact");
-            tracing::trace!("Fallback to hardhat artifact deserialization");
+            error!("Failed to deserialize compact artifact");
+            trace!("Fallback to hardhat artifact deserialization");
             let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
-            tracing::trace!("successfully deserialized hardhat artifact");
+            trace!("successfully deserialized hardhat artifact");
             Ok(artifact.into_contract_bytecode())
         }
     }
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index 06657c9c..2329a5a5 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -5,8 +5,8 @@ use crate::{
     error::{Result, SolcError},
     filter::{FilteredSource, FilteredSourceInfo, FilteredSources},
     resolver::GraphEdges,
-    utils, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Project, ProjectPathsConfig,
-    Source,
+    utils, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, OutputContext, Project,
+    ProjectPathsConfig, Source,
 };
 use semver::Version;
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -523,6 +523,11 @@ impl CacheEntry {
         self.artifacts.values().flat_map(|artifacts| artifacts.iter())
     }
 
+    /// Returns the artifact file for the contract and version pair
+    pub fn find_artifact(&self, contract: &str, version: &Version) -> Option<&PathBuf> {
+        self.artifacts.get(contract).and_then(|files| files.get(version))
+    }
+
     /// Iterator that yields all artifact files and their version
     pub fn artifacts_for_version<'a>(
         &'a self,
@@ -864,6 +869,13 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> {
         }
     }
 
+    pub fn output_ctx(&self) -> OutputContext {
+        match self {
+            ArtifactsCache::Ephemeral(_, _) => Default::default(),
+            ArtifactsCache::Cached(inner) => OutputContext::new(&inner.cache),
+        }
+    }
+
     pub fn project(&self) -> &'a Project<T> {
         match self {
             ArtifactsCache::Ephemeral(_, project) => project,
@@ -948,7 +960,8 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> {
                                         }).unwrap_or_default();
                                     if !retain {
                                         tracing::trace!(
-                                            "purging obsolete cached artifact for contract {} and version {}",
+                                            "purging obsolete cached artifact {:?} for contract {} and version {}",
+                                            f.file,
                                             name,
                                             f.version
                                         );
diff --git a/ethers-solc/src/compile/output/contracts.rs b/ethers-solc/src/compile/output/contracts.rs
index 09aab711..58787f8a 100644
--- a/ethers-solc/src/compile/output/contracts.rs
+++ b/ethers-solc/src/compile/output/contracts.rs
@@ -3,11 +3,13 @@ use crate::{
         contract::{CompactContractRef, Contract},
         FileToContractsMap,
     },
-    ArtifactFiles, ArtifactOutput,
+    files::{MappedArtifactFile, MappedArtifactFiles, MappedContract},
+    ArtifactOutput, OutputContext,
 };
 use semver::Version;
 use serde::{Deserialize, Serialize};
 use std::{collections::BTreeMap, ops::Deref, path::Path};
+use tracing::trace;
 
 /// file -> [(contract name  -> Contract + solc version)]
 #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
@@ -40,18 +42,46 @@ impl VersionedContracts {
     }
 
     /// Returns all the artifact files mapped with their contracts
-    pub(crate) fn artifact_files<T: ArtifactOutput + ?Sized>(&self) -> ArtifactFiles {
-        let mut output_files = ArtifactFiles::with_capacity(self.len());
+    ///
+    /// This will compute the appropriate output file paths but will _not_ write them.
+    /// The `ctx` is used to avoid possible conflicts
+    pub(crate) fn artifact_files<T: ArtifactOutput + ?Sized>(
+        &self,
+        ctx: &OutputContext,
+    ) -> MappedArtifactFiles {
+        let mut output_files = MappedArtifactFiles::with_capacity(self.len());
         for (file, contracts) in self.iter() {
             for (name, versioned_contracts) in contracts {
                 for contract in versioned_contracts {
-                    let output = if versioned_contracts.len() > 1 {
+                    // if an artifact for the contract already exists (from a previous compile job)
+                    // we reuse the path, this will make sure that even if there are conflicting
+                    // files (files for witch `T::output_file()` would return the same path) we use
+                    // consistent output paths
+                    let artifact_path = if let Some(existing_artifact) =
+                        ctx.existing_artifact(file, name, &contract.version).cloned()
+                    {
+                        trace!("use existing artifact file {:?}", existing_artifact,);
+                        existing_artifact
+                    } else if versioned_contracts.len() > 1 {
                         T::output_file_versioned(file, name, &contract.version)
                     } else {
                         T::output_file(file, name)
                     };
-                    let contract = (file.as_str(), name.as_str(), contract);
-                    output_files.entry(output).or_default().push(contract);
+
+                    trace!(
+                        "use artifact file {:?} for contract file {} {}",
+                        artifact_path,
+                        file,
+                        contract.version
+                    );
+                    let artifact = MappedArtifactFile::new(&artifact_path);
+                    let contract = MappedContract {
+                        file: file.as_str(),
+                        name: name.as_str(),
+                        contract,
+                        artifact_path,
+                    };
+                    output_files.entry(artifact).or_default().push(contract);
                 }
             }
         }
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index f0fdce64..fbb6d0a2 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -116,6 +116,7 @@ use crate::{
 };
 use rayon::prelude::*;
 use std::{collections::btree_map::BTreeMap, path::PathBuf, time::Instant};
+use tracing::trace;
 
 #[derive(Debug)]
 pub struct ProjectCompiler<'a, T: ArtifactOutput> {
@@ -231,6 +232,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
     ///   - sets proper source unit names
     ///   - check cache
     fn preprocess(self) -> Result<PreprocessedState<'a, T>> {
+        trace!("preprocessing");
         let Self { edges, project, mut sources, sparse_output } = self;
 
         // convert paths on windows to ensure consistency with the `CompilerOutput` `solc` emits,
@@ -260,6 +262,7 @@ struct PreprocessedState<'a, T: ArtifactOutput> {
 impl<'a, T: ArtifactOutput> PreprocessedState<'a, T> {
     /// advance to the next state by compiling all sources
     fn compile(self) -> Result<CompiledState<'a, T>> {
+        trace!("compiling");
         let PreprocessedState { sources, cache, sparse_output } = self;
         let project = cache.project();
         let mut output = sources.compile(
@@ -293,19 +296,21 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
     ///
     /// Writes all output contracts to disk if enabled in the `Project` and if the build was
     /// successful
+    #[tracing::instrument(skip_all, name = "write-artifacts")]
     fn write_artifacts(self) -> Result<ArtifactsState<'a, T>> {
         let CompiledState { output, cache } = self;
 
         let project = cache.project();
+        let ctx = cache.output_ctx();
         // write all artifacts via the handler but only if the build succeeded and project wasn't
         // configured with `no_artifacts == true`
         let compiled_artifacts = if project.no_artifacts {
-            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
+            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources, ctx)
         } else if output.has_error() {
-            tracing::trace!("skip writing cache file due to solc errors: {:?}", output.errors);
-            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
+            trace!("skip writing cache file due to solc errors: {:?}", output.errors);
+            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources, ctx)
         } else {
-            tracing::trace!(
+            trace!(
                 "handling artifact output for {} contracts and {} sources",
                 output.contracts.len(),
                 output.sources.len()
@@ -315,6 +320,7 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
                 &output.contracts,
                 &output.sources,
                 &project.paths,
+                ctx,
             )?;
 
             // emits all the build infos, if they exist
@@ -340,6 +346,7 @@ impl<'a, T: ArtifactOutput> ArtifactsState<'a, T> {
     ///
     /// this concludes the [`Project::compile()`] statemachine
     fn write_cache(self) -> Result<ProjectCompileOutput<T>> {
+        trace!("write cache");
         let ArtifactsState { output, cache, compiled_artifacts } = self;
         let ignored_error_codes = cache.project().ignored_error_codes.clone();
         let skip_write_to_disk = cache.project().no_artifacts || output.has_error();
@@ -404,9 +411,9 @@ impl CompilerSources {
             sources
                 .into_iter()
                 .map(|(solc, (version, sources))| {
-                    tracing::trace!("Filtering {} sources for {}", sources.len(), version);
+                    trace!("Filtering {} sources for {}", sources.len(), version);
                     let sources = cache.filter(sources, &version);
-                    tracing::trace!(
+                    trace!(
                         "Detected {} dirty sources {:?}",
                         sources.dirty().count(),
                         sources.dirty_files().collect::<Vec<_>>()
@@ -477,18 +484,14 @@ fn compile_sequential(
     create_build_info: bool,
 ) -> Result<AggregatedCompilerOutput> {
     let mut aggregated = AggregatedCompilerOutput::default();
-    tracing::trace!("compiling {} jobs sequentially", input.len());
+    trace!("compiling {} jobs sequentially", input.len());
     for (solc, (version, filtered_sources)) in input {
         if filtered_sources.is_empty() {
             // nothing to compile
-            tracing::trace!(
-                "skip solc {} {} for empty sources set",
-                solc.as_ref().display(),
-                version
-            );
+            trace!("skip solc {} {} for empty sources set", solc.as_ref().display(), version);
             continue
         }
-        tracing::trace!(
+        trace!(
             "compiling {} sources with solc \"{}\" {:?}",
             filtered_sources.len(),
             solc.as_ref().display(),
@@ -512,7 +515,7 @@ fn compile_sequential(
             if actually_dirty.is_empty() {
                 // nothing to compile for this particular language, all dirty files are in the other
                 // language set
-                tracing::trace!(
+                trace!(
                     "skip solc {} {} compilation of {} compiler input due to empty source set",
                     solc.as_ref().display(),
                     version,
@@ -527,7 +530,7 @@ fn compile_sequential(
                 .with_base_path(&paths.root)
                 .sanitized(&version);
 
-            tracing::trace!(
+            trace!(
                 "calling solc `{}` with {} sources {:?}",
                 version,
                 input.sources.len(),
@@ -538,8 +541,8 @@ fn compile_sequential(
             report::solc_spawn(&solc, &version, &input, &actually_dirty);
             let output = solc.compile(&input)?;
             report::solc_success(&solc, &version, &output, &start.elapsed());
-            tracing::trace!("compiled input, output has error: {}", output.has_error());
-            tracing::trace!("received compiler output: {:?}", output.contracts.keys());
+            trace!("compiled input, output has error: {}", output.has_error());
+            trace!("received compiler output: {:?}", output.contracts.keys());
 
             // if configured also create the build info
             if create_build_info {
@@ -564,21 +567,13 @@ fn compile_parallel(
     create_build_info: bool,
 ) -> Result<AggregatedCompilerOutput> {
     debug_assert!(num_jobs > 1);
-    tracing::trace!(
-        "compile {} sources in parallel using up to {} solc jobs",
-        input.len(),
-        num_jobs
-    );
+    trace!("compile {} sources in parallel using up to {} solc jobs", input.len(), num_jobs);
 
     let mut jobs = Vec::with_capacity(input.len());
     for (solc, (version, filtered_sources)) in input {
         if filtered_sources.is_empty() {
             // nothing to compile
-            tracing::trace!(
-                "skip solc {} {} for empty sources set",
-                solc.as_ref().display(),
-                version
-            );
+            trace!("skip solc {} {} for empty sources set", solc.as_ref().display(), version);
             continue
         }
 
@@ -599,7 +594,7 @@ fn compile_parallel(
             if actually_dirty.is_empty() {
                 // nothing to compile for this particular language, all dirty files are in the other
                 // language set
-                tracing::trace!(
+                trace!(
                     "skip solc {} {} compilation of {} compiler input due to empty source set",
                     solc.as_ref().display(),
                     version,
@@ -633,7 +628,7 @@ fn compile_parallel(
                 // set the reporter on this thread
                 let _guard = report::set_scoped(&scoped_report);
 
-                tracing::trace!(
+                trace!(
                     "calling solc `{}` {:?} with {} sources: {:?}",
                     version,
                     solc.args,
diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs
index fc00601c..72dbc95e 100644
--- a/ethers-solc/src/error.rs
+++ b/ethers-solc/src/error.rs
@@ -32,6 +32,13 @@ pub enum SolcError {
     Resolve(SolcIoError),
     #[error("File could not be resolved due to broken symlink: {0}.")]
     ResolveBadSymlink(SolcIoError),
+    #[error(
+        r#"Failed to resolve file: {0}.
+    --> {1:?}
+        {2:?}
+    Check configured remappings."#
+    )]
+    FailedResolveImport(SolcIoError, PathBuf, PathBuf),
     #[cfg(feature = "svm-solc")]
     #[error(transparent)]
     SvmError(#[from] svm::SolcVmError),
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index ffdf3131..1f2f65c3 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -859,8 +859,9 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
         layout: &ProjectPathsConfig,
+        ctx: OutputContext,
     ) -> Result<Artifacts<Self::Artifact>> {
-        self.artifacts_handler().on_output(contracts, sources, layout)
+        self.artifacts_handler().on_output(contracts, sources, layout, ctx)
     }
 
     fn write_contract_extras(&self, contract: &Contract, file: &Path) -> Result<()> {
@@ -933,8 +934,9 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
         &self,
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
+        ctx: OutputContext,
     ) -> Artifacts<Self::Artifact> {
-        self.artifacts_handler().output_to_artifacts(contracts, sources)
+        self.artifacts_handler().output_to_artifacts(contracts, sources, ctx)
     }
 
     fn standalone_source_file_to_artifact(
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 945e30e8..252af2ca 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -368,7 +368,20 @@ impl Graph {
                     &mut resolved_solc_include_paths,
                 ) {
                     Ok(import) => {
-                        add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
+                        add_node(&mut unresolved, &mut index, &mut resolved_imports, import)
+                            .map_err(|err| {
+                                match err {
+                                    SolcError::Resolve(err) => {
+                                        // make the error more verbose
+                                        SolcError::FailedResolveImport(
+                                            err,
+                                            node.path.clone(),
+                                            import_path.clone(),
+                                        )
+                                    }
+                                    _ => err,
+                                }
+                            })?
                     }
                     Err(err) => {
                         unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index 5dd25efc..e8d2c441 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -557,17 +557,18 @@ async fn can_abiencoderv2_output() {
     assert_eq!(res, person);
 }
 
-#[test]
-fn can_gen_multi_etherscan() {
-    abigen!(
-        MyContract, "etherscan:0xdAC17F958D2ee523a2206206994597C13D831ec7";
-        MyContract2, "etherscan:0x8418bb725b3ac45ec8fff3791dd8b4e0480cc2a2";
-    );
-
-    let provider = Arc::new(Provider::new(MockProvider::new()));
-    let _contract = MyContract::new(Address::default(), Arc::clone(&provider));
-    let _contract = MyContract2::new(Address::default(), provider);
-}
+// NOTE: this is commented out because this would result in compiler errors if key not set or
+// etherscan API not working #[test]
+// fn can_gen_multi_etherscan() {
+//     abigen!(
+//         MyContract, "etherscan:0xdAC17F958D2ee523a2206206994597C13D831ec7";
+//         MyContract2, "etherscan:0x8418bb725b3ac45ec8fff3791dd8b4e0480cc2a2";
+//     );
+//
+//     let provider = Arc::new(Provider::new(MockProvider::new()));
+//     let _contract = MyContract::new(Address::default(), Arc::clone(&provider));
+//     let _contract = MyContract2::new(Address::default(), provider);
+// }
 
 #[test]
 fn can_gen_reserved_word_field_names() {
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 4944e0ee..41fcb6f6 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -11,8 +11,8 @@ use ethers_solc::{
     info::ContractInfo,
     project_util::*,
     remappings::Remapping,
-    CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project, ProjectCompileOutput,
-    ProjectPathsConfig, Solc, TestFileFilter,
+    Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project,
+    ProjectCompileOutput, ProjectPathsConfig, Solc, TestFileFilter,
 };
 use pretty_assertions::assert_eq;
 use semver::Version;
@@ -2129,6 +2129,200 @@ fn can_handle_conflicting_files() {
     );
 }
 
+// <https://github.com/foundry-rs/foundry/issues/2843>
+#[test]
+fn can_handle_conflicting_files_recompile() {
+    let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
+
+    project
+        .add_source(
+            "A",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function foo() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function bar() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    // nothing to compile
+    let compiled = project.compile().unwrap();
+    assert!(compiled.is_unchanged());
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    let cache = SolFilesCache::read(project.cache_path()).unwrap();
+
+    let mut source_files = cache.files.keys().cloned().collect::<Vec<_>>();
+    source_files.sort_unstable();
+
+    assert_eq!(source_files, vec![PathBuf::from("src/A.sol"), PathBuf::from("src/inner/A.sol"),]);
+
+    let mut artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(artifacts.len(), 2);
+    let mut artifact_files = artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+
+    let expected_files = vec![PathBuf::from("A.sol/A.json"), PathBuf::from("inner/A.sol/A.json")];
+    assert_eq!(artifact_files, expected_files);
+
+    // overwrite conflicting nested file, effectively changing it
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+    contract A {
+    function bar() public{}
+    function baz() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let mut recompiled_artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    recompiled_artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(recompiled_artifacts.len(), 2);
+    let mut artifact_files =
+        recompiled_artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+    assert_eq!(artifact_files, expected_files);
+
+    // ensure that `a.sol/A.json` is unchanged
+    let outer = artifacts.find("src/A.sol", "A").unwrap();
+    let outer_recompiled = recompiled_artifacts.find("src/A.sol", "A").unwrap();
+    assert_eq!(outer, outer_recompiled);
+
+    let inner_recompiled = recompiled_artifacts.find("src/inner/A.sol", "A").unwrap();
+    assert!(inner_recompiled.get_abi().unwrap().functions.contains_key("baz"));
+}
+
+// <https://github.com/foundry-rs/foundry/issues/2843>
+#[test]
+fn can_handle_conflicting_files_case_sensitive_recompile() {
+    let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
+
+    project
+        .add_source(
+            "a",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function foo() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function bar() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    // nothing to compile
+    let compiled = project.compile().unwrap();
+    assert!(compiled.is_unchanged());
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    let cache = SolFilesCache::read(project.cache_path()).unwrap();
+
+    let mut source_files = cache.files.keys().cloned().collect::<Vec<_>>();
+    source_files.sort_unstable();
+
+    assert_eq!(source_files, vec![PathBuf::from("src/a.sol"), PathBuf::from("src/inner/A.sol"),]);
+
+    let mut artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(artifacts.len(), 2);
+    let mut artifact_files = artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+
+    let expected_files = vec![PathBuf::from("a.sol/A.json"), PathBuf::from("inner/A.sol/A.json")];
+    assert_eq!(artifact_files, expected_files);
+
+    // overwrite conflicting nested file, effectively changing it
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+    contract A {
+    function bar() public{}
+    function baz() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let mut recompiled_artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    recompiled_artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(recompiled_artifacts.len(), 2);
+    let mut artifact_files =
+        recompiled_artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+    assert_eq!(artifact_files, expected_files);
+
+    // ensure that `a.sol/A.json` is unchanged
+    let outer = artifacts.find("src/a.sol", "A").unwrap();
+    let outer_recompiled = recompiled_artifacts.find("src/a.sol", "A").unwrap();
+    assert_eq!(outer, outer_recompiled);
+
+    let inner_recompiled = recompiled_artifacts.find("src/inner/A.sol", "A").unwrap();
+    assert!(inner_recompiled.get_abi().unwrap().functions.contains_key("baz"));
+}
+
 #[test]
 fn can_checkout_repo() {
     let project = TempProject::checkout("transmissions11/solmate").unwrap();