Repository: gakonst/ethers-rs
Pull request #663 (2021-12-09 15:15:04)

feat: trezor support
<!--
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

Ref: #1
Adding trezor signing support, with the end goal of adding it to [foundry/cast](https://github.com/gakonst/foundry/tree/master/cast).

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

Forked a year old rust client (https://github.com/romanz/rust-trezor-api -> https://github.com/joshieDo/rust-trezor-api) and added the ethereum interface it lacked, along with updating the libusb (+rusb) to be compatible with ethers-rs.  I'm not much experienced in Rust, so an additional pair of eyes in my fork is most welcome.

On the `ethers-signers` implementation, I tried to follow the existing Ledger structure as much as possible.

- [x] get_address
- [x] sign_message
- [x] sign_tx
- [x] sign_eip1559
- [ ] sign_eip712

## ~~WIP~~

~~Fixing: Passphrase is being requested on every request~~

## Missing EIP712

I need to understand  EIP712 better from both sides, At first glance, it seems tricky to pass the intended data from `ethers-rs` to the `trezor client`. I was hoping that could be done in a future issue/PR. What do you think?

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d8f551e7..899b08d3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,7 @@ jobs:
       - name: Install Solc
         run: |
             mkdir -p "$HOME/bin"
-            wget -q https://github.com/ethereum/solidity/releases/download/v0.6.6/solc-static-linux -O $HOME/bin/solc
+            wget -q https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux -O $HOME/bin/solc
             chmod u+x "$HOME/bin/solc"
             export PATH=$HOME/bin:$PATH
             solc --version
@@ -76,7 +76,7 @@ jobs:
       - name: Install Solc
         run: |
             mkdir -p "$HOME/bin"
-            wget -q https://github.com/ethereum/solidity/releases/download/v0.6.6/solc-static-linux -O $HOME/bin/solc
+            wget -q https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux -O $HOME/bin/solc
             chmod u+x "$HOME/bin/solc"
             export PATH=$HOME/bin:$PATH
             solc --version
@@ -194,7 +194,7 @@ jobs:
       - name: Install Solc
         run: |
             mkdir -p "$HOME/bin"
-            wget -q https://github.com/ethereum/solidity/releases/download/v0.6.6/solc-static-linux -O $HOME/bin/solc
+            wget -q https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux -O $HOME/bin/solc
             chmod u+x "$HOME/bin/solc"
             export PATH=$HOME/bin:$PATH
             solc --version
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7d268b4..1099c294 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,20 @@
   [597](https://github.com/gakonst/ethers-rs/pull/597)
 - Implement hex display format for `ethers::core::Bytes`
   [#624](https://github.com/gakonst/ethers-rs/pull/624).
+- Fix `fee_history` to first try with `block_count` encoded as a hex `QUANTITY`.
+  [#668](https://github.com/gakonst/ethers-rs/pull/668)
+
+## ethers-contract-abigen
+
+- Implement snowtrace and polygonscan on par with the etherscan integration
+  [#666](https://github.com/gakonst/ethers-rs/pull/666).
 
 ## ethers-solc
 
 ### Unreleased
 
+- Add support for hardhat artifacts [#677](https://github.com/gakonst/ethers-rs/pull/677)
+- Add more utility functions to the `Artifact` trait [#673](https://github.com/gakonst/ethers-rs/pull/673)
 - Return cached artifacts from project `compile` when the cache only contains
   some files
 - Add support for library linking and make `Bytecode`'s `object` filed an `enum BytecodeObject`
@@ -94,6 +103,8 @@
 
 ### Unreleased
 
+- Substitute output tuples with rust struct types for function calls
+  [#664](https://github.com/gakonst/ethers-rs/pull/664)
 - Add AbiType implementation during EthAbiType expansion
   [#647](https://github.com/gakonst/ethers-rs/pull/647)
 - fix Etherscan conditional HTTP support
diff --git a/Cargo.lock b/Cargo.lock
index cf084d06..7ff8ba4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -287,6 +287,15 @@ dependencies = [
  "generic-array 0.14.4",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "block-modes"
 version = "0.8.1"
@@ -776,6 +785,15 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "crypto-common"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567569e659735adb39ff2d4c20600f7cd78be5471f8c58ab162bce3c03fdbc5f"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "crypto-mac"
 version = "0.8.0"
@@ -907,6 +925,17 @@ dependencies = [
  "generic-array 0.14.4",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8549e6bfdecd113b7e221fe60b433087f6957387a20f8118ebca9b12af19143d"
+dependencies = [
+ "block-buffer 0.10.0",
+ "crypto-common",
+ "generic-array 0.14.4",
+]
+
 [[package]]
 name = "dirs-next"
 version = "2.0.0"
@@ -1304,6 +1333,7 @@ dependencies = [
  "tracing",
  "tracing-futures",
  "tracing-subscriber",
+ "trezor-client",
  "yubihsm",
 ]
 
@@ -1314,12 +1344,13 @@ dependencies = [
  "colored",
  "criterion",
  "ethers-core",
+ "fs_extra",
  "futures-util",
  "getrandom 0.2.3",
  "glob",
  "hex",
  "home",
- "md-5",
+ "md-5 0.10.0",
  "num_cpus",
  "once_cell",
  "regex",
@@ -1412,6 +1443,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "fs_extra"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
+
 [[package]]
 name = "fuchsia-cprng"
 version = "0.1.1"
@@ -1957,6 +1994,15 @@ dependencies = [
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "md-5"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6a38fc55c8bbc10058782919516f88826e70320db6d206aebc49611d24216ae"
+dependencies = [
+ "digest 0.10.0",
+]
+
 [[package]]
 name = "memchr"
 version = "2.4.1"
@@ -2407,6 +2453,12 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "protobuf"
+version = "2.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754"
+
 [[package]]
 name = "quote"
 version = "1.0.10"
@@ -2792,7 +2844,7 @@ dependencies = [
  "http",
  "hyper",
  "log",
- "md-5",
+ "md-5 0.9.1",
  "percent-encoding",
  "pin-project-lite",
  "rusoto_credential",
@@ -3603,6 +3655,21 @@ dependencies = [
  "tracing-log",
 ]
 
+[[package]]
+name = "trezor-client"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff94fab279e0d429d762c289f9727f37a0f1b8207ea4795f09c11caad009046f"
+dependencies = [
+ "byteorder",
+ "hex",
+ "hidapi",
+ "log",
+ "primitive-types",
+ "protobuf",
+ "rusb",
+]
+
 [[package]]
 name = "try-lock"
 version = "0.2.3"
diff --git a/Cargo.toml b/Cargo.toml
index 12434931..60fa90bd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -67,6 +67,7 @@ openssl = ["ethers-providers/openssl"]
 dev-rpc = ["ethers-providers/dev-rpc"]
 ## signers
 ledger = ["ethers-signers/ledger"]
+trezor = ["ethers-signers/trezor"]
 yubi = ["ethers-signers/yubi"]
 ## contracts
 abigen = ["ethers-contract/abigen"]
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
index f48eabc4..64667cf1 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs
@@ -38,8 +38,7 @@ impl Context {
         let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect();
         let filter_methods = sorted_events
             .values()
-            .map(std::ops::Deref::deref)
-            .flatten()
+            .flat_map(std::ops::Deref::deref)
             .map(|event| self.expand_filter(event))
             .collect::<Vec<_>>();
 
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index eb40aa8f..7827eba2 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -28,8 +28,7 @@ impl Context {
         let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect();
         let functions = sorted_functions
             .values()
-            .map(std::ops::Deref::deref)
-            .flatten()
+            .flat_map(std::ops::Deref::deref)
             .map(|function| {
                 let signature = function.abi_signature();
                 self.expand_function(function, aliases.get(&signature).cloned())
@@ -50,7 +49,7 @@ impl Context {
         alias: Option<&MethodAlias>,
     ) -> Result<TokenStream> {
         let call_name = expand_call_struct_name(function, alias);
-        let fields = self.expand_input_pairs(function)?;
+        let fields = self.expand_input_params(function)?;
         // expand as a tuple if all fields are anonymous
         let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty());
         let call_type_definition = if all_anonymous_fields {
@@ -163,16 +162,34 @@ impl Context {
     }
 
     /// Expands to the `name : type` pairs of the function's inputs
-    fn expand_input_pairs(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
+    fn expand_input_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
         let mut args = Vec::with_capacity(fun.inputs.len());
         for (idx, param) in fun.inputs.iter().enumerate() {
             let name = util::expand_input_name(idx, &param.name);
-            let ty = self.expand_input_param(fun, &param.name, &param.kind)?;
+            let ty = self.expand_input_param_type(fun, &param.name, &param.kind)?;
             args.push((name, ty));
         }
         Ok(args)
     }
 
+    /// Expands to the return type of a function
+    fn expand_outputs(&self, fun: &Function) -> Result<TokenStream> {
+        let mut outputs = Vec::with_capacity(fun.outputs.len());
+        for param in fun.outputs.iter() {
+            let ty = self.expand_output_param_type(fun, param, &param.kind)?;
+            outputs.push(ty);
+        }
+
+        let return_ty = match outputs.len() {
+            0 => quote! { () },
+            1 => outputs[0].clone(),
+            _ => {
+                quote! { (#( #outputs ),*) }
+            }
+        };
+        Ok(return_ty)
+    }
+
     /// Expands the arguments for the call that eventually calls the contract
     fn expand_contract_call_args(&self, fun: &Function) -> Result<TokenStream> {
         let mut call_args = Vec::with_capacity(fun.inputs.len());
@@ -202,7 +219,8 @@ impl Context {
         Ok(call_args)
     }
 
-    fn expand_input_param(
+    /// returns the Tokenstream for the corresponding rust type of the param
+    fn expand_input_param_type(
         &self,
         fun: &Function,
         param: &str,
@@ -210,13 +228,13 @@ impl Context {
     ) -> Result<TokenStream> {
         match kind {
             ParamType::Array(ty) => {
-                let ty = self.expand_input_param(fun, param, ty)?;
+                let ty = self.expand_input_param_type(fun, param, ty)?;
                 Ok(quote! {
                     ::std::vec::Vec<#ty>
                 })
             }
             ParamType::FixedArray(ty, size) => {
-                let ty = self.expand_input_param(fun, param, ty)?;
+                let ty = self.expand_input_param_type(fun, param, ty)?;
                 let size = *size;
                 Ok(quote! {[#ty; #size]})
             }
@@ -235,27 +253,61 @@ impl Context {
         }
     }
 
+    /// returns the Tokenstream for the corresponding rust type of the output param
+    fn expand_output_param_type(
+        &self,
+        fun: &Function,
+        param: &Param,
+        kind: &ParamType,
+    ) -> Result<TokenStream> {
+        match kind {
+            ParamType::Array(ty) => {
+                let ty = self.expand_output_param_type(fun, param, ty)?;
+                Ok(quote! {
+                    ::std::vec::Vec<#ty>
+                })
+            }
+            ParamType::FixedArray(ty, size) => {
+                let ty = self.expand_output_param_type(fun, param, ty)?;
+                let size = *size;
+                Ok(quote! {[#ty; #size]})
+            }
+            ParamType::Tuple(_) => {
+                let ty = if let Some(rust_struct_name) =
+                    param.internal_type.as_ref().and_then(|s| {
+                        self.internal_structs.get_function_output_struct_type(&fun.name, s)
+                    }) {
+                    let ident = util::ident(rust_struct_name);
+                    quote! {#ident}
+                } else {
+                    types::expand(kind)?
+                };
+                Ok(ty)
+            }
+            _ => types::expand(kind),
+        }
+    }
+
     /// Expands a single function with the given alias
     fn expand_function(
         &self,
         function: &Function,
         alias: Option<MethodAlias>,
     ) -> Result<TokenStream> {
-        let name = expand_function_name(function, alias.as_ref());
-        let selector = expand_selector(function.selector());
-
-        // TODO use structs
-        let outputs = expand_fn_outputs(&function.outputs)?;
-
         let ethers_contract = ethers_contract_crate();
 
-        let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
+        let name = expand_function_name(function, alias.as_ref());
+        let selector = expand_selector(function.selector());
 
         let contract_args = self.expand_contract_call_args(function)?;
         let function_params =
-            self.expand_input_pairs(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty });
+            self.expand_input_params(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty });
         let function_params = quote! { #( , #function_params )* };
 
+        let outputs = self.expand_outputs(function)?;
+
+        let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
+
         let doc = util::expand_doc(&format!(
             "Calls the contract's `{}` (0x{}) function",
             function.name,
@@ -478,20 +530,6 @@ impl Context {
     }
 }
 
-fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
-    match outputs.len() {
-        0 => Ok(quote! { () }),
-        1 => types::expand(&outputs[0].kind),
-        _ => {
-            let types = outputs
-                .iter()
-                .map(|param| types::expand(&param.kind))
-                .collect::<Result<Vec<_>>>()?;
-            Ok(quote! { (#( #types ),*) })
-        }
-    }
-}
-
 fn expand_selector(selector: Selector) -> TokenStream {
     let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
     quote! { [#( #bytes ),*] }
@@ -575,6 +613,20 @@ mod tests {
 
     use super::*;
 
+    fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
+        match outputs.len() {
+            0 => Ok(quote! { () }),
+            1 => types::expand(&outputs[0].kind),
+            _ => {
+                let types = outputs
+                    .iter()
+                    .map(|param| types::expand(&param.kind))
+                    .collect::<Result<Vec<_>>>()?;
+                Ok(quote! { (#( #types ),*) })
+            }
+        }
+    }
+
     // packs the argument in a tuple to be used for the contract call
     fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
         let names = inputs
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index 526a8dcf..64a90bcd 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -312,6 +312,22 @@ impl InternalStructs {
             .map(String::as_str)
     }
 
+    /// Returns the name of the rust type that will be generated if the given output is a struct
+    /// NOTE: this does not account for arrays or fixed arrays
+    pub fn get_function_output_struct_type(
+        &self,
+        function: &str,
+        internal_type: &str,
+    ) -> Option<&str> {
+        self.outputs
+            .get(function)
+            .and_then(|outputs| {
+                outputs.iter().find(|s| s.as_str() == struct_type_identifier(internal_type))
+            })
+            .and_then(|id| self.rust_type_names.get(id))
+            .map(String::as_str)
+    }
+
     /// Returns the mapping table of abi `internal type identifier -> rust type`
     pub fn rust_type_names(&self) -> &HashMap<String, String> {
         &self.rust_type_names
diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs
index 01136c43..0bcef005 100644
--- a/ethers-contract/ethers-contract-abigen/src/source.rs
+++ b/ethers-contract/ethers-contract-abigen/src/source.rs
@@ -23,6 +23,12 @@ pub enum Source {
     /// An address of a mainnet contract that has been verified on Etherscan.io.
     Etherscan(Address),
 
+    /// An address of a mainnet contract that has been verified on Polygonscan.com.
+    Polygonscan(Address),
+
+    /// An address of a mainnet contract that has been verified on snowtrace.io.
+    Snowtrace(Address),
+
     /// The package identifier of an npm package with a path to a Truffle
     /// artifact or ABI to be retrieved from `unpkg.io`.
     Npm(String),
@@ -91,7 +97,7 @@ impl Source {
         let url = base.join(source)?;
 
         match url.scheme() {
-            "file" => Ok(Source::local(source.to_string())),
+            "file" => Ok(Source::local(source)),
             "http" | "https" => match url.host_str() {
                 Some("etherscan.io") => Source::etherscan(
                     url.path()
@@ -99,9 +105,23 @@ impl Source {
                         .next()
                         .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
                 ),
+                Some("polygonscan.com") => Source::polygonscan(
+                    url.path()
+                        .rsplit('/')
+                        .next()
+                        .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
+                ),
+                Some("snowtrace.io") => Source::snowtrace(
+                    url.path()
+                        .rsplit('/')
+                        .next()
+                        .ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
+                ),
                 _ => Ok(Source::Http(url)),
             },
             "etherscan" => Source::etherscan(url.path()),
+            "polygonscan" => Source::polygonscan(url.path()),
+            "snowtrace" => Source::snowtrace(url.path()),
             "npm" => Ok(Source::npm(url.path())),
             _ => Err(anyhow!("unsupported URL '{}'", url)),
         }
@@ -130,6 +150,26 @@ impl Source {
         Ok(Source::Etherscan(address))
     }
 
+    /// Creates an Polygonscan source from an address string.
+    pub fn polygonscan<S>(address: S) -> Result<Self>
+    where
+        S: AsRef<str>,
+    {
+        let address = util::parse_address(address)
+            .context("failed to parse address for Polygonscan source")?;
+        Ok(Source::Polygonscan(address))
+    }
+
+    /// Creates an Snowtrace source from an address string.
+    pub fn snowtrace<S>(address: S) -> Result<Self>
+    where
+        S: AsRef<str>,
+    {
+        let address =
+            util::parse_address(address).context("failed to parse address for Snowtrace source")?;
+        Ok(Source::Snowtrace(address))
+    }
+
     /// Creates an Etherscan source from an address string.
     pub fn npm<S>(package_path: S) -> Self
     where
@@ -144,10 +184,12 @@ impl Source {
     pub fn get(&self) -> Result<String> {
         cfg_if! {
              if #[cfg(target_arch = "wasm32")] {
-                 match self {
+                match self {
                     Source::Local(path) => get_local_contract(path),
                     Source::Http(_) =>   panic!("Http abi location are not supported for wasm"),
                     Source::Etherscan(_) => panic!("Etherscan abi location are not supported for wasm"),
+                    Source::Polygonscan(_) => panic!("Polygonscan abi location are not supported for wasm"),
+                    Source::Snowtrace(_) => panic!("Snowtrace abi location are not supported for wasm"),
                     Source::Npm(_) => panic!("npm abi location are not supported for wasm"),
                     Source::String(abi) => Ok(abi.clone()),
                 }
@@ -155,7 +197,9 @@ impl Source {
                 match self {
                     Source::Local(path) => get_local_contract(path),
                     Source::Http(url) => get_http_contract(url),
-                    Source::Etherscan(address) => get_etherscan_contract(*address),
+                    Source::Etherscan(address) => get_etherscan_contract(*address, "etherscan.io"),
+                    Source::Polygonscan(address) => get_etherscan_contract(*address, "polygonscan.com"),
+                    Source::Snowtrace(address) => get_etherscan_contract(*address, "snowtrace.io"),
                     Source::Npm(package) => get_npm_contract(package),
                     Source::String(abi) => Ok(abi.clone()),
                 }
@@ -211,20 +255,27 @@ fn get_http_contract(url: &Url) -> Result<String> {
 /// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an
 /// artifact JSON for compatibility with the code generation facilities.
 #[cfg(not(target_arch = "wasm32"))]
-fn get_etherscan_contract(address: Address) -> Result<String> {
+fn get_etherscan_contract(address: Address, domain: &str) -> Result<String> {
     // NOTE: We do not retrieve the bytecode since deploying contracts with the
     //   same bytecode is unreliable as the libraries have already linked and
     //   probably don't reference anything when deploying on other networks.
 
-    let api_key =
-        env::var("ETHERSCAN_API_KEY").map(|key| format!("&apikey={}", key)).unwrap_or_default();
+    let api_key = {
+        let key_res = match domain {
+            "etherscan.io" => env::var("ETHERSCAN_API_KEY").ok(),
+            "polygonscan.com" => env::var("POLYGONSCAN_API_KEY").ok(),
+            "snowtrace.io" => env::var("SNOWTRACE_API_KEY").ok(),
+            _ => None,
+        };
+        key_res.map(|key| format!("&apikey={}", key)).unwrap_or_default()
+    };
 
     let abi_url = format!(
-        "http://api.etherscan.io/api\
-         ?module=contract&action=getabi&address={:?}&format=raw{}",
-        address, api_key,
+        "http://api.{}/api?module=contract&action=getabi&address={:?}&format=raw{}",
+        domain, address, api_key,
     );
-    let abi = util::http_get(&abi_url).context("failed to retrieve ABI from Etherscan.io")?;
+    let abi =
+        util::http_get(&abi_url).context(format!("failed to retrieve ABI from {}", domain))?;
     Ok(abi)
 }
 
@@ -256,10 +307,26 @@ mod tests {
                 "etherscan:0x0001020304050607080910111213141516171819",
                 Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
             ),
+            (
+                "polygonscan:0x0001020304050607080910111213141516171819",
+                Source::polygonscan("0x0001020304050607080910111213141516171819").unwrap(),
+            ),
+            (
+                "snowtrace:0x0001020304050607080910111213141516171819",
+                Source::snowtrace("0x0001020304050607080910111213141516171819").unwrap(),
+            ),
             (
                 "https://etherscan.io/address/0x0001020304050607080910111213141516171819",
                 Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
             ),
+            (
+                "https://polygonscan.com/address/0x0001020304050607080910111213141516171819",
+                Source::polygonscan("0x0001020304050607080910111213141516171819").unwrap(),
+            ),
+            (
+                "https://snowtrace.io/address/0x0001020304050607080910111213141516171819",
+                Source::snowtrace("0x0001020304050607080910111213141516171819").unwrap(),
+            ),
             (
                 "npm:@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json",
                 Source::npm("@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json"),
diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs
index 138f744c..f8858db4 100644
--- a/ethers-contract/ethers-contract-abigen/src/util.rs
+++ b/ethers-contract/ethers-contract-abigen/src/util.rs
@@ -2,7 +2,6 @@ use ethers_core::types::Address;
 use std::path::PathBuf;
 
 use anyhow::{anyhow, Result};
-use cfg_if::cfg_if;
 use inflector::Inflector;
 use proc_macro2::{Ident, Literal, Span, TokenStream};
 use quote::quote;
@@ -84,12 +83,13 @@ where
 }
 
 /// Perform an HTTP GET request and return the contents of the response.
+#[cfg(not(target_arch = "wasm32"))]
 pub fn http_get(_url: &str) -> Result<String> {
-    cfg_if! {
-        if #[cfg(any(target_arch = "wasm32", not(feature = "reqwest")))]{
-            Err(anyhow!("HTTP is unsupported"))
-        } else {
+    cfg_if::cfg_if! {
+        if #[cfg(feature = "reqwest")]{
             Ok(reqwest::blocking::get(_url)?.text()?)
+        } else {
+            Err(anyhow!("HTTP is unsupported"))
         }
     }
 }
diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs
index 096969d6..ebc4225d 100644
--- a/ethers-contract/src/multicall/mod.rs
+++ b/ethers-contract/src/multicall/mod.rs
@@ -191,7 +191,7 @@ impl<M: Middleware> Multicall<M> {
     /// If more than the maximum number of supported calls are added. The maximum
     /// limits is constrained due to tokenization/detokenization support for tuples
     pub fn add_call<D: Detokenize>(&mut self, call: ContractCall<M, D>) -> &mut Self {
-        assert!(!(self.calls.len() >= 16), "Cannot support more than {} calls", 16);
+        assert!(self.calls.len() < 16, "Cannot support more than {} calls", 16);
 
         match (call.tx.to(), call.tx.data()) {
             (Some(NameOrAddress::Address(target)), Some(data)) => {
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 722d0e83..64a9c28e 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -79,7 +79,12 @@ pub fn format_units<T: Into<U256>, K: Into<Units>>(amount: T, units: K) -> Strin
     let amount = amount.into();
     let amount_decimals = amount % U256::from(10_u128.pow(units.as_num()));
     let amount_integer = amount / U256::from(10_u128.pow(units.as_num()));
-    amount_integer.to_string() + "." + &amount_decimals.to_string()
+    format!(
+        "{}.{:0width$}",
+        amount_integer,
+        amount_decimals.as_u128(),
+        width = units.as_num() as usize
+    )
 }
 
 /// Converts the input to a U256 and converts from Ether to Wei.
@@ -398,6 +403,9 @@ mod tests {
 
         let eth = format_units(U256::from_dec_str("1395633240123456789").unwrap(), "ether");
         assert_eq!(eth, "1.395633240123456789");
+
+        let eth = format_units(U256::from_dec_str("1005633240123456789").unwrap(), "ether");
+        assert_eq!(eth, "1.005633240123456789");
     }
 
     #[test]
diff --git a/ethers-etherscan/src/gas.rs b/ethers-etherscan/src/gas.rs
index bdbbc640..e9c7e1de 100644
--- a/ethers-etherscan/src/gas.rs
+++ b/ethers-etherscan/src/gas.rs
@@ -123,7 +123,7 @@ mod tests {
             assert!(oracle.fast_gas_price > 0);
             assert!(oracle.last_block > 0);
             assert!(oracle.suggested_base_fee > 0.0);
-            assert!(oracle.gas_used_ratio.len() > 0);
+            assert!(!oracle.gas_used_ratio.is_empty());
         })
         .await
     }
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index 01c9b678..c4b152ed 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -848,12 +848,13 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
         self.subscribe([logs, filter]).await
     }
 
-    async fn fee_history<T: Into<U256> + serde::Serialize + Send + Sync>(
+    async fn fee_history<T: Into<U256> + Send + Sync>(
         &self,
         block_count: T,
         last_block: BlockNumber,
         reward_percentiles: &[f64],
     ) -> Result<FeeHistory, Self::Error> {
+        let block_count = block_count.into();
         let last_block = utils::serialize(&last_block);
         let reward_percentiles = utils::serialize(&reward_percentiles);
 
@@ -868,7 +869,7 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
         .or(self
             .request(
                 "eth_feeHistory",
-                [utils::serialize(&block_count.into().as_u64()), last_block, reward_percentiles],
+                [utils::serialize(&block_count.as_u64()), last_block, reward_percentiles],
             )
             .await)
     }
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 84c2b914..46691e67 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -28,6 +28,7 @@ yubihsm = { version = "0.39.0", features = ["secp256k1", "http", "usb"], optiona
 futures-util = "0.3.18"
 futures-executor = "0.3.18"
 semver = "1.0.4"
+trezor-client = { version = "0.0.3", optional = true, default-features = false, features = ["f_ethereum"] }
 
 # aws
 rusoto_core = { version = "0.47.0", optional = true }
@@ -56,3 +57,4 @@ celo = ["ethers-core/celo"]
 ledger = ["coins-ledger"]
 yubi = ["yubihsm"]
 aws = ["rusoto_core", "rusoto_kms", "tracing", "tracing-futures", "spki"]
+trezor = ["trezor-client"]
diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs
index 31e20d06..558d0995 100644
--- a/ethers-signers/src/ledger/app.rs
+++ b/ethers-signers/src/ledger/app.rs
@@ -206,7 +206,7 @@ impl LedgerEthereum {
         let mut bytes = vec![depth as u8];
         for derivation_index in elements {
             let hardened = derivation_index.contains('\'');
-            let mut index = derivation_index.replace("'", "").parse::<u32>().unwrap();
+            let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
             if hardened {
                 index |= 0x80000000;
             }
diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs
index 4b94355c..f127fa30 100644
--- a/ethers-signers/src/lib.rs
+++ b/ethers-signers/src/lib.rs
@@ -20,6 +20,14 @@ pub use ledger::{
     types::{DerivationType as HDPath, LedgerError},
 };
 
+#[cfg(feature = "trezor")]
+mod trezor;
+#[cfg(feature = "trezor")]
+pub use trezor::{
+    app::TrezorEthereum as Trezor,
+    types::{DerivationType as TrezorHDPath, TrezorError},
+};
+
 #[cfg(feature = "yubi")]
 pub use yubihsm;
 
diff --git a/ethers-signers/src/trezor/app.rs b/ethers-signers/src/trezor/app.rs
new file mode 100644
index 00000000..8e299ea6
--- /dev/null
+++ b/ethers-signers/src/trezor/app.rs
@@ -0,0 +1,339 @@
+#![allow(unused)]
+use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor};
+
+use futures_executor::block_on;
+use futures_util::lock::Mutex;
+
+use ethers_core::{
+    types::{
+        transaction::{eip2718::TypedTransaction, eip712::Eip712},
+        Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256,
+    },
+    utils::keccak256,
+};
+use std::convert::TryFrom;
+use thiserror::Error;
+
+use super::types::*;
+
+/// A Trezor Ethereum App.
+///
+/// This is a simple wrapper around the [Trezor transport](Trezor)
+#[derive(Debug)]
+pub struct TrezorEthereum {
+    derivation: DerivationType,
+    session_id: Vec<u8>,
+    pub(crate) chain_id: u64,
+    pub(crate) address: Address,
+}
+
+const FIRMWARE_MIN_VERSION: &str = ">=2.4.2";
+
+impl TrezorEthereum {
+    pub async fn new(derivation: DerivationType, chain_id: u64) -> Result<Self, TrezorError> {
+        let mut blank = Self {
+            derivation: derivation.clone(),
+            chain_id,
+            address: Address::from([0_u8; 20]),
+            session_id: vec![],
+        };
+
+        // Check if reachable
+        blank.initate_session()?;
+        blank.address = blank.get_address_with_path(&derivation).await?;
+        Ok(blank)
+    }
+
+    fn check_version(version: String) -> Result<(), TrezorError> {
+        let req = semver::VersionReq::parse(FIRMWARE_MIN_VERSION)?;
+        let version = semver::Version::parse(&version)?;
+
+        // Enforce firmware version is greater than FIRMWARE_MIN_VERSION
+        if !req.matches(&version) {
+            return Err(TrezorError::UnsupportedFirmwareVersion(FIRMWARE_MIN_VERSION.to_string()))
+        }
+
+        Ok(())
+    }
+
+    fn initate_session(&mut self) -> Result<(), TrezorError> {
+        let mut client = trezor_client::unique(false)?;
+        client.init_device(None)?;
+
+        let features = client.features().ok_or(TrezorError::FeaturesError)?;
+
+        Self::check_version(format!(
+            "{}.{}.{}",
+            features.get_major_version(),
+            features.get_minor_version(),
+            features.get_patch_version()
+        ))?;
+
+        self.session_id = features.get_session_id().to_vec();
+
+        Ok(())
+    }
+
+    /// You need to drop(client) once you're done with it
+    fn get_client(&self, session_id: Vec<u8>) -> Result<Trezor, TrezorError> {
+        let mut client = trezor_client::unique(false)?;
+        client.init_device(Some(session_id))?;
+        Ok(client)
+    }
+
+    /// Get the account which corresponds to our derivation path
+    pub async fn get_address(&self) -> Result<Address, TrezorError> {
+        Ok(self.get_address_with_path(&self.derivation).await?)
+    }
+
+    /// Gets the account which corresponds to the provided derivation path
+    pub async fn get_address_with_path(
+        &self,
+        derivation: &DerivationType,
+    ) -> Result<Address, TrezorError> {
+        let mut client = self.get_client(self.session_id.clone())?;
+
+        let address_str = client.ethereum_get_address(Self::convert_path(derivation))?;
+
+        let mut address = [0; 20];
+        address.copy_from_slice(&hex::decode(&address_str[2..])?);
+
+        Ok(Address::from(address))
+    }
+
+    /// Signs an Ethereum transaction (requires confirmation on the Trezor)
+    pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, TrezorError> {
+        let mut client = self.get_client(self.session_id.clone())?;
+
+        let arr_path = Self::convert_path(&self.derivation);
+
+        let transaction = TrezorTransaction::load(tx)?;
+
+        let signature = match tx {
+            TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx(
+                arr_path,
+                transaction.nonce,
+                transaction.gas_price,
+                transaction.gas,
+                transaction.to,
+                transaction.value,
+                transaction.data,
+                self.chain_id,
+            )?,
+            TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx(
+                arr_path,
+                transaction.nonce,
+                transaction.gas,
+                transaction.to,
+                transaction.value,
+                transaction.data,
+                self.chain_id,
+                transaction.max_fee_per_gas,
+                transaction.max_priority_fee_per_gas,
+                transaction.access_list,
+            )?,
+        };
+
+        Ok(Signature { r: signature.r, s: signature.s, v: signature.v })
+    }
+
+    /// Signs an ethereum personal message
+    pub async fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, TrezorError> {
+        let message = message.as_ref();
+        let mut client = self.get_client(self.session_id.clone())?;
+        let apath = Self::convert_path(&self.derivation);
+
+        let signature = client.ethereum_sign_message(message.into(), apath)?;
+
+        Ok(Signature { r: signature.r, s: signature.s, v: signature.v })
+    }
+
+    /// Signs an EIP712 encoded domain separator and message
+    pub async fn sign_typed_struct<T>(&self, payload: &T) -> Result<Signature, TrezorError>
+    where
+        T: Eip712,
+    {
+        unimplemented!()
+    }
+
+    // helper which converts a derivation path to [u32]
+    fn convert_path(derivation: &DerivationType) -> Vec<u32> {
+        let derivation = derivation.to_string();
+        let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
+        let depth = elements.len();
+
+        let mut path = vec![];
+        for derivation_index in elements {
+            let hardened = derivation_index.contains('\'');
+            let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
+            if hardened {
+                index |= 0x80000000;
+            }
+            path.push(index);
+        }
+
+        path
+    }
+}
+
+#[cfg(all(test, feature = "trezor"))]
+mod tests {
+    use super::*;
+    use crate::Signer;
+    use ethers_contract::EthAbiType;
+    use ethers_core::types::{
+        transaction::{
+            eip2930::{AccessList, AccessListItem},
+            eip712::Eip712,
+        },
+        Address, Eip1559TransactionRequest, TransactionRequest, I256, U256,
+    };
+    use ethers_derive_eip712::*;
+    use std::str::FromStr;
+
+    #[derive(Debug, Clone, Eip712, EthAbiType)]
+    #[eip712(
+        name = "Eip712Test",
+        version = "1",
+        chain_id = 1,
+        verifying_contract = "0x0000000000000000000000000000000000000001",
+        salt = "eip712-test-75F0CCte"
+    )]
+    struct FooBar {
+        foo: I256,
+        bar: U256,
+        fizz: Vec<u8>,
+        buzz: [u8; 32],
+        far: String,
+        out: Address,
+    }
+
+    #[tokio::test]
+    #[ignore]
+    // Replace this with your ETH addresses.
+    async fn test_get_address() {
+        // Instantiate it with the default trezor derivation path
+        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(1), 1).await.unwrap();
+        assert_eq!(
+            trezor.get_address().await.unwrap(),
+            "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
+        );
+        assert_eq!(
+            trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(),
+            "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
+        );
+    }
+
+    #[tokio::test]
+    #[ignore]
+    async fn test_sign_tx() {
+        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
+
+        // approve uni v2 router 0xff
+        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
+
+        let tx_req = TransactionRequest::new()
+            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
+            .gas(1000000)
+            .gas_price(400e9 as u64)
+            .nonce(5)
+            .data(data)
+            .value(ethers_core::utils::parse_ether(100).unwrap())
+            .into();
+        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
+    }
+
+    #[tokio::test]
+    #[ignore]
+    async fn test_sign_big_data_tx() {
+        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
+
+        // invalid data
+        let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + &"aa".to_string()).unwrap();
+        let tx_req = TransactionRequest::new()
+            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
+            .gas(1000000)
+            .gas_price(400e9 as u64)
+            .nonce(5)
+            .data(big_data)
+            .value(ethers_core::utils::parse_ether(100).unwrap())
+            .into();
+        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
+    }
+
+    #[tokio::test]
+    #[ignore]
+    async fn test_sign_eip1559_tx() {
+        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
+
+        // approve uni v2 router 0xff
+        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
+
+        let lst = AccessList(vec![
+            AccessListItem {
+                address: "0x8ba1f109551bd432803012645ac136ddd64dba72".parse().unwrap(),
+                storage_keys: vec![
+                    "0x0000000000000000000000000000000000000000000000000000000000000000"
+                        .parse()
+                        .unwrap(),
+                    "0x0000000000000000000000000000000000000000000000000000000000000042"
+                        .parse()
+                        .unwrap(),
+                ],
+            },
+            AccessListItem {
+                address: "0x2ed7afa17473e17ac59908f088b4371d28585476".parse().unwrap(),
+                storage_keys: vec![
+                    "0x0000000000000000000000000000000000000000000000000000000000000000"
+                        .parse()
+                        .unwrap(),
+                    "0x0000000000000000000000000000000000000000000000000000000000000042"
+                        .parse()
+                        .unwrap(),
+                ],
+            },
+        ]);
+
+        let tx_req = Eip1559TransactionRequest::new()
+            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
+            .gas(1000000)
+            .max_fee_per_gas(400e9 as u64)
+            .max_priority_fee_per_gas(400e9 as u64)
+            .nonce(5)
+            .data(data)
+            .access_list(lst)
+            .value(ethers_core::utils::parse_ether(100).unwrap())
+            .into();
+
+        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
+    }
+
+    #[tokio::test]
+    #[ignore]
+    async fn test_sign_message() {
+        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
+        let message = "hello world";
+        let sig = trezor.sign_message(message).await.unwrap();
+        let addr = trezor.get_address().await.unwrap();
+        sig.verify(message, addr).unwrap();
+    }
+
+    #[tokio::test]
+    #[ignore]
+    async fn test_sign_eip712_struct() {
+        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1u64).await.unwrap();
+
+        let foo_bar = FooBar {
+            foo: I256::from(10),
+            bar: U256::from(20),
+            fizz: b"fizz".to_vec(),
+            buzz: keccak256("buzz"),
+            far: String::from("space"),
+            out: Address::from([0; 20]),
+        };
+
+        let sig = trezor.sign_typed_struct(&foo_bar).await.expect("failed to sign typed data");
+        let foo_bar_hash = foo_bar.encode_eip712().unwrap();
+        sig.verify(foo_bar_hash, trezor.address).unwrap();
+    }
+}
diff --git a/ethers-signers/src/trezor/mod.rs b/ethers-signers/src/trezor/mod.rs
new file mode 100644
index 00000000..060959b6
--- /dev/null
+++ b/ethers-signers/src/trezor/mod.rs
@@ -0,0 +1,52 @@
+pub mod app;
+pub mod types;
+
+use crate::Signer;
+use app::TrezorEthereum;
+use async_trait::async_trait;
+use ethers_core::types::{
+    transaction::{eip2718::TypedTransaction, eip712::Eip712},
+    Address, Signature,
+};
+use types::TrezorError;
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl Signer for TrezorEthereum {
+    type Error = TrezorError;
+
+    /// Signs the hash of the provided message after prefixing it
+    async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
+        &self,
+        message: S,
+    ) -> Result<Signature, Self::Error> {
+        self.sign_message(message).await
+    }
+
+    /// Signs the transaction
+    async fn sign_transaction(&self, message: &TypedTransaction) -> Result<Signature, Self::Error> {
+        self.sign_tx(message).await
+    }
+
+    /// Signs a EIP712 derived struct
+    async fn sign_typed_data<T: Eip712 + Send + Sync>(
+        &self,
+        payload: &T,
+    ) -> Result<Signature, Self::Error> {
+        self.sign_typed_struct(payload).await
+    }
+
+    /// Returns the signer's Ethereum Address
+    fn address(&self) -> Address {
+        self.address
+    }
+
+    fn with_chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
+        self.chain_id = chain_id.into();
+        self
+    }
+
+    fn chain_id(&self) -> u64 {
+        self.chain_id
+    }
+}
diff --git a/ethers-signers/src/trezor/types.rs b/ethers-signers/src/trezor/types.rs
new file mode 100644
index 00000000..2942d496
--- /dev/null
+++ b/ethers-signers/src/trezor/types.rs
@@ -0,0 +1,132 @@
+#![allow(clippy::upper_case_acronyms)]
+//! Helpers for interacting with the Ethereum Trezor App
+//! [Official Docs](https://github.com/TrezorHQ/app-ethereum/blob/master/doc/ethapp.asc)
+use std::fmt;
+use thiserror::Error;
+
+use ethers_core::types::{transaction::eip2718::TypedTransaction, NameOrAddress, U256};
+use trezor_client::client::AccessListItem as Trezor_AccessListItem;
+
+#[derive(Clone, Debug)]
+/// Trezor wallet type
+pub enum DerivationType {
+    /// Trezor Live-generated HD path
+    TrezorLive(usize),
+    /// Any other path. Attention! Trezor by default forbids custom derivation paths
+    /// Run trezorctl set safety-checks prompt, to allow it
+    Other(String),
+}
+
+impl fmt::Display for DerivationType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        write!(
+            f,
+            "{}",
+            match self {
+                DerivationType::TrezorLive(index) => format!("m/44'/60'/{}'/0/0", index),
+                DerivationType::Other(inner) => inner.to_owned(),
+            }
+        )
+    }
+}
+
+#[derive(Error, Debug)]
+/// Error when using the Trezor transport
+pub enum TrezorError {
+    /// Underlying Trezor transport error
+    #[error(transparent)]
+    TrezorError(#[from] trezor_client::error::Error),
+    #[error("Trezor was not able to retrieve device features")]
+    FeaturesError,
+    #[error("Not able to unpack value for TrezorTransaction.")]
+    DataError,
+    #[error(transparent)]
+    /// Error when converting from a hex string
+    HexError(#[from] hex::FromHexError),
+    #[error(transparent)]
+    /// Error when converting a semver requirement
+    SemVerError(#[from] semver::Error),
+    /// Error when signing EIP712 struct with not compatible Trezor ETH app
+    #[error("Trezor ethereum app requires at least version: {0:?}")]
+    UnsupportedFirmwareVersion(String),
+}
+
+/// Trezor Transaction Struct
+pub struct TrezorTransaction {
+    pub nonce: Vec<u8>,
+    pub gas: Vec<u8>,
+    pub gas_price: Vec<u8>,
+    pub value: Vec<u8>,
+    pub to: String,
+    pub data: Vec<u8>,
+    pub max_fee_per_gas: Vec<u8>,
+    pub max_priority_fee_per_gas: Vec<u8>,
+    pub access_list: Vec<Trezor_AccessListItem>,
+}
+
+impl TrezorTransaction {
+    fn to_trimmed_big_endian(_value: &U256) -> Vec<u8> {
+        let mut trimmed_value = [0_u8; 32];
+        _value.to_big_endian(&mut trimmed_value);
+        trimmed_value[_value.leading_zeros() as usize / 8..].to_vec()
+    }
+
+    pub fn load(tx: &TypedTransaction) -> Result<Self, TrezorError> {
+        let to: String = match tx.to().ok_or(TrezorError::DataError)? {
+            NameOrAddress::Name(_) => unimplemented!(),
+            NameOrAddress::Address(value) => format!("0x{}", hex::encode(value)),
+        };
+
+        let nonce = Self::to_trimmed_big_endian(tx.nonce().ok_or(TrezorError::DataError)?);
+        let gas = Self::to_trimmed_big_endian(tx.gas().ok_or(TrezorError::DataError)?);
+        let gas_price = Self::to_trimmed_big_endian(&tx.gas_price().ok_or(TrezorError::DataError)?);
+        let value = Self::to_trimmed_big_endian(tx.value().ok_or(TrezorError::DataError)?);
+        let data = tx.data().ok_or(TrezorError::DataError)?.to_vec();
+
+        match tx {
+            TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => Ok(Self {
+                nonce,
+                gas,
+                gas_price,
+                value,
+                to,
+                data,
+                max_fee_per_gas: vec![],
+                max_priority_fee_per_gas: vec![],
+                access_list: vec![],
+            }),
+            TypedTransaction::Eip1559(eip1559_tx) => {
+                let max_fee_per_gas = Self::to_trimmed_big_endian(
+                    &eip1559_tx.max_fee_per_gas.ok_or(TrezorError::DataError)?,
+                );
+                let max_priority_fee_per_gas = Self::to_trimmed_big_endian(
+                    &eip1559_tx.max_priority_fee_per_gas.ok_or(TrezorError::DataError)?,
+                );
+
+                let mut access_list: Vec<Trezor_AccessListItem> = Vec::new();
+                for item in &eip1559_tx.access_list.0 {
+                    let address: String = format!("0x{}", hex::encode(item.address));
+                    let mut storage_keys: Vec<Vec<u8>> = Vec::new();
+
+                    for key in &item.storage_keys {
+                        storage_keys.push(key.as_bytes().to_vec())
+                    }
+
+                    access_list.push(Trezor_AccessListItem { address, storage_keys })
+                }
+
+                Ok(Self {
+                    nonce,
+                    gas,
+                    gas_price,
+                    value,
+                    to,
+                    data,
+                    max_fee_per_gas,
+                    max_priority_fee_per_gas,
+                    access_list,
+                })
+            }
+        }
+    }
+}
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index c7bb762f..eb13c035 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -23,7 +23,7 @@ tokio = { version = "1.12.0", default-features = false, features = ["process", "
 futures-util = { version = "0.3.18", optional = true }
 once_cell = "1.8.0"
 regex = "1.5.4"
-md-5 = "0.9.1"
+md-5 = "0.10.0"
 thiserror = "1.0.30"
 hex = "0.4.3"
 colored = "2.0.0"
@@ -32,11 +32,13 @@ glob = "0.3.0"
 tracing = "0.1.29"
 num_cpus = "1.13.0"
 tiny-keccak = { version = "2.0.2", default-features = false }
+tempdir = { version = "0.3.7", optional = true }
+fs_extra = { version = "1.2.0", optional = true }
 
-[target.'cfg(not(any(target_arch = "x86", target_arch = "x86_64")))'.dependencies]
+[target.'cfg(any(not(any(target_arch = "x86", target_arch = "x86_64")), target_env = "msvc"))'.dependencies]
 sha2 = { version = "0.9.8", default-features = false }
 
-[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
+[target.'cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(target_env = "msvc")))'.dependencies]
 sha2 = { version = "0.9.8", default-features = false, features = ["asm"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ -48,14 +50,21 @@ getrandom = { version = "0.2", features = ["js"] }
 
 [dev-dependencies]
 criterion = { version = "0.3", features = ["async_tokio"] }
-tokio = { version = "1.12.0", features = ["full"] }
 tempdir = "0.3.7"
+tokio = { version = "1.12.0", features = ["full"] }
 
 [[bench]]
 name = "compile_many"
 harness = false
 
+[[test]]
+name = "project"
+path = "tests/project.rs"
+required-features = ["project-util"]
+
 [features]
 async = ["tokio", "futures-util"]
 full = ["async", "svm"]
+# Utilities for creating and testing project workspaces
+project-util = ["tempdir", "fs_extra"]
 tests = []
diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs
index 2fb84238..20719dcc 100644
--- a/ethers-solc/src/artifacts.rs
+++ b/ethers-solc/src/artifacts.rs
@@ -6,12 +6,12 @@ use md5::Digest;
 use semver::Version;
 use std::{
     collections::BTreeMap,
-    fmt, fs, io,
+    fmt, fs,
     path::{Path, PathBuf},
     str::FromStr,
 };
 
-use crate::{compile::*, remappings::Remapping, utils};
+use crate::{compile::*, error::SolcIoError, remappings::Remapping, utils};
 use ethers_core::abi::Address;
 use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
 
@@ -30,7 +30,7 @@ pub struct CompilerInput {
 
 impl CompilerInput {
     /// Reads all contracts found under the path
-    pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
+    pub fn new(path: impl AsRef<Path>) -> Result<Self, SolcIoError> {
         Source::read_all_from(path.as_ref()).map(Self::with_sources)
     }
 
@@ -185,7 +185,7 @@ impl Default for Settings {
             optimizer: Default::default(),
             metadata: None,
             output_selection: Self::default_output_selection(),
-            evm_version: Some(EvmVersion::Istanbul),
+            evm_version: Some(EvmVersion::default()),
             libraries: Default::default(),
             remappings: Default::default(),
         }
@@ -225,15 +225,21 @@ impl Default for Optimizer {
 pub enum EvmVersion {
     Homestead,
     TangerineWhistle,
-    SpuriusDragon,
+    SpuriousDragon,
+    Byzantium,
     Constantinople,
     Petersburg,
     Istanbul,
     Berlin,
-    Byzantium,
     London,
 }
 
+impl Default for EvmVersion {
+    fn default() -> Self {
+        Self::London
+    }
+}
+
 impl EvmVersion {
     /// Checks against the given solidity `semver::Version`
     pub fn normalize_version(self, version: &Version) -> Option<EvmVersion> {
@@ -267,7 +273,7 @@ impl fmt::Display for EvmVersion {
         let string = match self {
             EvmVersion::Homestead => "homestead",
             EvmVersion::TangerineWhistle => "tangerineWhistle",
-            EvmVersion::SpuriusDragon => "spuriusDragon",
+            EvmVersion::SpuriousDragon => "spuriousDragon",
             EvmVersion::Constantinople => "constantinople",
             EvmVersion::Petersburg => "petersburg",
             EvmVersion::Istanbul => "istanbul",
@@ -286,7 +292,7 @@ impl FromStr for EvmVersion {
         match s {
             "homestead" => Ok(EvmVersion::Homestead),
             "tangerineWhistle" => Ok(EvmVersion::TangerineWhistle),
-            "spuriusDragon" => Ok(EvmVersion::SpuriusDragon),
+            "spuriousDragon" => Ok(EvmVersion::SpuriousDragon),
             "constantinople" => Ok(EvmVersion::Constantinople),
             "petersburg" => Ok(EvmVersion::Petersburg),
             "istanbul" => Ok(EvmVersion::Istanbul),
@@ -376,17 +382,18 @@ pub struct Source {
 
 impl Source {
     /// Reads the file content
-    pub fn read(file: impl AsRef<Path>) -> io::Result<Self> {
-        Ok(Self { content: fs::read_to_string(file.as_ref())? })
+    pub fn read(file: impl AsRef<Path>) -> Result<Self, SolcIoError> {
+        let file = file.as_ref();
+        Ok(Self { content: fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))? })
     }
 
     /// Finds all source files under the given dir path and reads them all
-    pub fn read_all_from(dir: impl AsRef<Path>) -> io::Result<Sources> {
-        Self::read_all(utils::source_files(dir)?)
+    pub fn read_all_from(dir: impl AsRef<Path>) -> Result<Sources, SolcIoError> {
+        Self::read_all(utils::source_files(dir))
     }
 
     /// Reads all files
-    pub fn read_all<T, I>(files: I) -> io::Result<Sources>
+    pub fn read_all<T, I>(files: I) -> Result<Sources, SolcIoError>
     where
         I: IntoIterator<Item = T>,
         T: Into<PathBuf>,
@@ -415,17 +422,22 @@ impl Source {
 #[cfg(feature = "async")]
 impl Source {
     /// async version of `Self::read`
-    pub async fn async_read(file: impl AsRef<Path>) -> io::Result<Self> {
-        Ok(Self { content: tokio::fs::read_to_string(file.as_ref()).await? })
+    pub async fn async_read(file: impl AsRef<Path>) -> Result<Self, SolcIoError> {
+        let file = file.as_ref();
+        Ok(Self {
+            content: tokio::fs::read_to_string(file)
+                .await
+                .map_err(|err| SolcIoError::new(err, file))?,
+        })
     }
 
     /// Finds all source files under the given dir path and reads them all
-    pub async fn async_read_all_from(dir: impl AsRef<Path>) -> io::Result<Sources> {
-        Self::async_read_all(utils::source_files(dir.as_ref())?).await
+    pub async fn async_read_all_from(dir: impl AsRef<Path>) -> Result<Sources, SolcIoError> {
+        Self::async_read_all(utils::source_files(dir.as_ref())).await
     }
 
     /// async version of `Self::read_all`
-    pub async fn async_read_all<T, I>(files: I) -> io::Result<Sources>
+    pub async fn async_read_all<T, I>(files: I) -> Result<Sources, SolcIoError>
     where
         I: IntoIterator<Item = T>,
         T: Into<PathBuf>,
@@ -511,6 +523,7 @@ pub struct OutputDiagnostics<'a> {
 }
 
 impl<'a> OutputDiagnostics<'a> {
+    /// Returns true if there is at least one error of high severity
     pub fn has_error(&self) -> bool {
         self.errors.iter().any(|err| err.severity.is_error())
     }
@@ -559,7 +572,7 @@ pub struct Contract {
 }
 
 /// Minimal representation of a contract's abi with bytecode
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
 pub struct CompactContract {
     /// The Ethereum Contract ABI. If empty, it is represented as an empty
     /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html
@@ -571,7 +584,7 @@ pub struct CompactContract {
 }
 
 impl CompactContract {
-    /// Returns the contents of this type as a single
+    /// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
     pub fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
         (
             self.abi,
@@ -592,6 +605,20 @@ impl CompactContract {
     }
 }
 
+impl From<serde_json::Value> for CompactContract {
+    fn from(mut val: serde_json::Value) -> Self {
+        if let Some(map) = val.as_object_mut() {
+            let abi = map.remove("abi").and_then(|val| serde_json::from_value(val).ok());
+            let bin = map.remove("bin").and_then(|val| serde_json::from_value(val).ok());
+            let bin_runtime =
+                map.remove("bin-runtime").and_then(|val| serde_json::from_value(val).ok());
+            Self { abi, bin, bin_runtime }
+        } else {
+            CompactContract::default()
+        }
+    }
+}
+
 impl From<Contract> for CompactContract {
     fn from(c: Contract) -> Self {
         let (bin, bin_runtime) = if let Some(evm) = c.evm {
@@ -920,6 +947,13 @@ impl BytecodeObject {
     }
 }
 
+// Returns a not deployable bytecode by default as "0x"
+impl Default for BytecodeObject {
+    fn default() -> Self {
+        BytecodeObject::Unlinked("0x".to_string())
+    }
+}
+
 impl AsRef<[u8]> for BytecodeObject {
     fn as_ref(&self) -> &[u8] {
         match self {
@@ -1217,7 +1251,13 @@ where
     D: Deserializer<'de>,
 {
     let value = String::deserialize(d)?;
-    Ok(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into())
+    if let Some(value) = value.strip_prefix("0x") {
+        hex::decode(value)
+    } else {
+        hex::decode(&value)
+    }
+    .map(Into::into)
+    .map_err(|e| serde::de::Error::custom(e.to_string()))
 }
 
 pub fn deserialize_opt_bytes<'de, D>(d: D) -> std::result::Result<Option<Bytes>, D::Error>
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index 4a5957e6..fb9fb908 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -16,6 +16,13 @@ use std::{
 /// Hardhat format version
 const HH_FORMAT_VERSION: &str = "hh-sol-cache-2";
 
+/// ethers-rs format version
+///
+/// `ethers-solc` uses a different format version id, but the actual format is consistent with
+/// hardhat This allows ethers-solc to detect if the cache file was written by hardhat or
+/// `ethers-solc`
+const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-cache-1";
+
 /// The file name of the default cache file
 pub const SOLIDITY_FILES_CACHE_FILENAME: &str = "solidity-files-cache.json";
 
@@ -42,12 +49,22 @@ impl SolFilesCache {
         SolFilesCacheBuilder::default()
     }
 
+    /// Whether this cache's format is the hardhat format identifier
+    pub fn is_hardhat_format(&self) -> bool {
+        self.format == HH_FORMAT_VERSION
+    }
+
+    /// Whether this cache's format is our custom format identifier
+    pub fn is_ethers_format(&self) -> bool {
+        self.format == ETHERS_FORMAT_VERSION
+    }
+
     /// Reads the cache json file from the given path
     #[tracing::instrument(skip_all, name = "sol-files-cache::read")]
     pub fn read(path: impl AsRef<Path>) -> Result<Self> {
         let path = path.as_ref();
         tracing::trace!("reading solfiles cache at {}", path.display());
-        let file = fs::File::open(path)?;
+        let file = fs::File::open(path).map_err(|err| SolcError::io(err, path))?;
         let file = std::io::BufReader::new(file);
         let cache = serde_json::from_reader(file)?;
         tracing::trace!("done");
@@ -57,7 +74,7 @@ impl SolFilesCache {
     /// Write the cache to json file
     pub fn write(&self, path: impl AsRef<Path>) -> Result<()> {
         let path = path.as_ref();
-        let file = fs::File::create(path)?;
+        let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
         tracing::trace!("writing cache to json file");
         serde_json::to_writer_pretty(file, self)?;
         tracing::trace!("cache file located: {}", path.display());
@@ -161,7 +178,7 @@ impl SolFilesCache {
         })
     }
 
-    /// Reads all cached artifacts from disk
+    /// Reads all cached artifacts from disk using the given ArtifactOutput handler
     pub fn read_artifacts<T: ArtifactOutput>(
         &self,
         artifacts_root: &Path,
@@ -181,13 +198,16 @@ impl SolFilesCache {
 #[cfg(feature = "async")]
 impl SolFilesCache {
     pub async fn async_read(path: impl AsRef<Path>) -> Result<Self> {
-        let content = tokio::fs::read_to_string(path.as_ref()).await?;
+        let path = path.as_ref();
+        let content =
+            tokio::fs::read_to_string(path).await.map_err(|err| SolcError::io(err, path))?;
         Ok(serde_json::from_str(&content)?)
     }
 
     pub async fn async_write(&self, path: impl AsRef<Path>) -> Result<()> {
+        let path = path.as_ref();
         let content = serde_json::to_vec_pretty(self)?;
-        Ok(tokio::fs::write(path.as_ref(), content).await?)
+        Ok(tokio::fs::write(path, content).await.map_err(|err| SolcError::io(err, path))?)
     }
 }
 
@@ -215,16 +235,22 @@ impl SolFilesCacheBuilder {
     }
 
     pub fn insert_files(self, sources: Sources, dest: Option<PathBuf>) -> Result<SolFilesCache> {
-        let format = self.format.unwrap_or_else(|| HH_FORMAT_VERSION.to_string());
+        let format = self.format.unwrap_or_else(|| ETHERS_FORMAT_VERSION.to_string());
         let solc_config =
             self.solc_config.map(Ok).unwrap_or_else(|| SolcConfig::builder().build())?;
 
-        let root = self.root.map(Ok).unwrap_or_else(std::env::current_dir)?;
+        let root = self
+            .root
+            .map(Ok)
+            .unwrap_or_else(std::env::current_dir)
+            .map_err(|err| SolcError::io(err, "."))?;
 
         let mut files = BTreeMap::new();
         for (file, source) in sources {
-            let last_modification_date = fs::metadata(&file)?
-                .modified()?
+            let last_modification_date = fs::metadata(&file)
+                .map_err(|err| SolcError::io(err, file.clone()))?
+                .modified()
+                .map_err(|err| SolcError::io(err, file.clone()))?
                 .duration_since(UNIX_EPOCH)
                 .map_err(|err| SolcError::solc(err.to_string()))?
                 .as_millis() as u64;
@@ -251,7 +277,9 @@ impl SolFilesCacheBuilder {
             if dest.exists() {
                 // read the existing cache and extend it by the files that changed
                 // (if we just wrote to the cache file, we'd overwrite the existing data)
-                let reader = std::io::BufReader::new(File::open(dest)?);
+                let reader = std::io::BufReader::new(
+                    File::open(dest).map_err(|err| SolcError::io(err, dest))?,
+                );
                 let mut cache: SolFilesCache = serde_json::from_reader(reader)?;
                 assert_eq!(cache.format, format);
                 cache.files.extend(files);
diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs
index d52726da..0521bee0 100644
--- a/ethers-solc/src/compile.rs
+++ b/ethers-solc/src/compile.rs
@@ -202,7 +202,7 @@ impl Solc {
     pub fn version_req(source: &Source) -> Result<VersionReq> {
         let version = utils::find_version_pragma(&source.content)
             .ok_or(SolcError::PragmaNotFound)?
-            .replace(" ", ",");
+            .replace(' ', ",");
 
         // Somehow, Solidity semver without an operator is considered to be "exact",
         // but lack of operator automatically marks the operator as Caret, so we need
@@ -247,7 +247,8 @@ impl Solc {
         let version = self.version_short()?;
         let mut version_path = svm::version_path(version.to_string().as_str());
         version_path.push(format!("solc-{}", version.to_string().as_str()));
-        let content = std::fs::read(version_path)?;
+        let content =
+            std::fs::read(&version_path).map_err(|err| SolcError::io(err, version_path))?;
 
         use sha2::Digest;
         let mut hasher = sha2::Sha256::new();
@@ -265,6 +266,7 @@ impl Solc {
 
     /// Convenience function for compiling all sources under the given path
     pub fn compile_source(&self, path: impl AsRef<Path>) -> Result<CompilerOutput> {
+        let path = path.as_ref();
         self.compile(&CompilerInput::new(path)?)
     }
 
@@ -302,11 +304,12 @@ impl Solc {
             .stdin(Stdio::piped())
             .stderr(Stdio::piped())
             .stdout(Stdio::piped())
-            .spawn()?;
+            .spawn()
+            .map_err(|err| SolcError::io(err, &self.solc))?;
         let stdin = child.stdin.take().unwrap();
 
         serde_json::to_writer(stdin, input)?;
-        compile_output(child.wait_with_output()?)
+        compile_output(child.wait_with_output().map_err(|err| SolcError::io(err, &self.solc))?)
     }
 
     pub fn version_short(&self) -> Result<Version> {
@@ -322,7 +325,8 @@ impl Solc {
                 .stdin(Stdio::piped())
                 .stderr(Stdio::piped())
                 .stdout(Stdio::piped())
-                .output()?,
+                .output()
+                .map_err(|err| SolcError::io(err, &self.solc))?,
         )
     }
 }
@@ -358,15 +362,19 @@ impl Solc {
         use tokio::io::AsyncWriteExt;
         let content = serde_json::to_vec(input)?;
         let mut child = tokio::process::Command::new(&self.solc)
+            .args(&self.args)
             .arg("--standard-json")
             .stdin(Stdio::piped())
             .stderr(Stdio::piped())
             .stdout(Stdio::piped())
-            .spawn()?;
+            .spawn()
+            .map_err(|err| SolcError::io(err, &self.solc))?;
         let stdin = child.stdin.as_mut().unwrap();
-        stdin.write_all(&content).await?;
-        stdin.flush().await?;
-        compile_output(child.wait_with_output().await?)
+        stdin.write_all(&content).await.map_err(|err| SolcError::io(err, &self.solc))?;
+        stdin.flush().await.map_err(|err| SolcError::io(err, &self.solc))?;
+        compile_output(
+            child.wait_with_output().await.map_err(|err| SolcError::io(err, &self.solc))?,
+        )
     }
 
     pub async fn async_version(&self) -> Result<Version> {
@@ -376,9 +384,11 @@ impl Solc {
                 .stdin(Stdio::piped())
                 .stderr(Stdio::piped())
                 .stdout(Stdio::piped())
-                .spawn()?
+                .spawn()
+                .map_err(|err| SolcError::io(err, &self.solc))?
                 .wait_with_output()
-                .await?,
+                .await
+                .map_err(|err| SolcError::io(err, &self.solc))?,
         )
     }
 
@@ -469,7 +479,8 @@ fn version_from_output(output: Output) -> Result<Version> {
             .stdout
             .lines()
             .last()
-            .ok_or_else(|| SolcError::solc("version not found in solc output"))??;
+            .ok_or_else(|| SolcError::solc("version not found in solc output"))?
+            .map_err(|err| SolcError::msg(format!("Failed to read output: {}", err)))?;
         // NOTE: semver doesn't like `+` in g++ in build metadata which is invalid semver
         Ok(Version::from_str(&version.trim_start_matches("Version: ").replace(".g++", ".gcc"))?)
     } else {
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 4f97dd1e..7332529e 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -1,12 +1,13 @@
 use crate::{
     artifacts::{CompactContract, CompactContractRef, Contract, Settings},
     cache::SOLIDITY_FILES_CACHE_FILENAME,
-    error::Result,
+    error::{Result, SolcError, SolcIoError},
+    hh::HardhatArtifact,
     remappings::Remapping,
     CompilerOutput,
 };
 use ethers_core::{abi::Abi, types::Bytes};
-use serde::{Deserialize, Serialize};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use std::{
     collections::BTreeMap,
     convert::TryFrom,
@@ -50,12 +51,27 @@ impl ProjectPathsConfig {
 
     /// Creates a new config with the current directory as the root
     pub fn current_hardhat() -> Result<Self> {
-        Self::hardhat(std::env::current_dir()?)
+        Self::hardhat(std::env::current_dir().map_err(|err| SolcError::io(err, "."))?)
     }
 
     /// Creates a new config with the current directory as the root
     pub fn current_dapptools() -> Result<Self> {
-        Self::dapptools(std::env::current_dir()?)
+        Self::dapptools(std::env::current_dir().map_err(|err| SolcError::io(err, "."))?)
+    }
+
+    /// Creates all configured dirs and files
+    pub fn create_all(&self) -> std::result::Result<(), SolcIoError> {
+        if let Some(parent) = self.cache.parent() {
+            fs::create_dir_all(parent).map_err(|err| SolcIoError::new(err, parent))?;
+        }
+        fs::create_dir_all(&self.artifacts)
+            .map_err(|err| SolcIoError::new(err, &self.artifacts))?;
+        fs::create_dir_all(&self.sources).map_err(|err| SolcIoError::new(err, &self.sources))?;
+        fs::create_dir_all(&self.tests).map_err(|err| SolcIoError::new(err, &self.tests))?;
+        for lib in &self.libraries {
+            fs::create_dir_all(lib).map_err(|err| SolcIoError::new(err, lib))?;
+        }
+        Ok(())
     }
 }
 
@@ -67,7 +83,8 @@ pub enum PathStyle {
 
 impl PathStyle {
     pub fn paths(&self, root: impl AsRef<Path>) -> Result<ProjectPathsConfig> {
-        let root = std::fs::canonicalize(root)?;
+        let root = root.as_ref();
+        let root = std::fs::canonicalize(root).map_err(|err| SolcError::io(err, root))?;
 
         Ok(match self {
             PathStyle::Dapptools => ProjectPathsConfig::builder()
@@ -156,11 +173,9 @@ impl ProjectPathsConfigBuilder {
         self
     }
 
-    pub fn build(self) -> io::Result<ProjectPathsConfig> {
-        let root = self.root.map(Ok).unwrap_or_else(std::env::current_dir)?;
-        let root = std::fs::canonicalize(root)?;
-
-        Ok(ProjectPathsConfig {
+    pub fn build_with_root(self, root: impl Into<PathBuf>) -> ProjectPathsConfig {
+        let root = root.into();
+        ProjectPathsConfig {
             cache: self
                 .cache
                 .unwrap_or_else(|| root.join("cache").join(SOLIDITY_FILES_CACHE_FILENAME)),
@@ -170,7 +185,18 @@ impl ProjectPathsConfigBuilder {
             libraries: self.libraries.unwrap_or_default(),
             remappings: self.remappings.unwrap_or_default(),
             root,
-        })
+        }
+    }
+
+    pub fn build(self) -> std::result::Result<ProjectPathsConfig, SolcIoError> {
+        let root = self
+            .root
+            .clone()
+            .map(Ok)
+            .unwrap_or_else(std::env::current_dir)
+            .map_err(|err| SolcIoError::new(err, "."))?;
+        let root = std::fs::canonicalize(&root).map_err(|err| SolcIoError::new(err, &root))?;
+        Ok(self.build_with_root(root))
     }
 }
 
@@ -218,31 +244,34 @@ impl SolcConfigBuilder {
 pub type Artifacts<T> = BTreeMap<String, BTreeMap<String, T>>;
 
 pub trait Artifact {
+    /// Returns the artifact's `Abi` and bytecode
     fn into_inner(self) -> (Option<Abi>, Option<Bytes>);
+
+    /// Turns the artifact into a container type for abi, bytecode and deployed bytecode
+    fn into_compact_contract(self) -> CompactContract;
+
+    /// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
+    fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>);
 }
 
-impl Artifact for CompactContract {
+impl<T: Into<CompactContract>> Artifact for T {
     fn into_inner(self) -> (Option<Abi>, Option<Bytes>) {
-        (self.abi, self.bin.and_then(|bin| bin.into_bytes()))
+        let artifact = self.into_compact_contract();
+        (artifact.abi, artifact.bin.and_then(|bin| bin.into_bytes()))
     }
-}
 
-impl Artifact for serde_json::Value {
-    fn into_inner(self) -> (Option<Abi>, Option<Bytes>) {
-        let abi = self.get("abi").map(|abi| {
-            serde_json::from_value::<Abi>(abi.clone()).expect("could not get artifact abi")
-        });
-        let bytecode = self.get("bin").map(|bin| {
-            serde_json::from_value::<Bytes>(bin.clone()).expect("could not get artifact bytecode")
-        });
+    fn into_compact_contract(self) -> CompactContract {
+        self.into()
+    }
 
-        (abi, bytecode)
+    fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
+        self.into_compact_contract().into_parts()
     }
 }
 
 pub trait ArtifactOutput {
     /// How Artifacts are stored
-    type Artifact: Artifact;
+    type Artifact: Artifact + DeserializeOwned;
 
     /// Handle the compiler output.
     fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()>;
@@ -282,7 +311,12 @@ pub trait ArtifactOutput {
         root.as_ref().join(Self::output_file(contract_file, name)).exists()
     }
 
-    fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact>;
+    fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
+        let path = path.as_ref();
+        let file = fs::File::open(path).map_err(|err| SolcError::io(err, path))?;
+        let file = io::BufReader::new(file);
+        Ok(serde_json::from_reader(file)?)
+    }
 
     /// Read the cached artifacts from disk
     fn read_cached_artifacts<T, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
@@ -300,21 +334,22 @@ pub trait ArtifactOutput {
     }
 
     /// Convert a contract to the artifact type
-    fn contract_to_artifact(contract: Contract) -> Self::Artifact;
+    fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact;
 
     /// Convert the compiler output into a set of artifacts
     fn output_to_artifacts(output: CompilerOutput) -> Artifacts<Self::Artifact> {
         output
             .contracts
             .into_iter()
-            .map(|(s, contracts)| {
-                (
-                    s,
-                    contracts
-                        .into_iter()
-                        .map(|(s, c)| (s, Self::contract_to_artifact(c)))
-                        .collect(),
-                )
+            .map(|(file, contracts)| {
+                let contracts = contracts
+                    .into_iter()
+                    .map(|(name, c)| {
+                        let contract = Self::contract_to_artifact(&file, &name, c);
+                        (name, contract)
+                    })
+                    .collect();
+                (file, contracts)
             })
             .collect()
     }
@@ -337,48 +372,62 @@ impl ArtifactOutput for MinimalCombinedArtifacts {
     type Artifact = CompactContract;
 
     fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
-        fs::create_dir_all(&layout.artifacts)?;
+        fs::create_dir_all(&layout.artifacts)
+            .map_err(|err| SolcError::msg(format!("Failed to create artifacts dir: {}", err)))?;
         for (file, contracts) in output.contracts.iter() {
             for (name, contract) in contracts {
                 let artifact = Self::output_file(file, name);
                 let file = layout.artifacts.join(artifact);
                 if let Some(parent) = file.parent() {
-                    fs::create_dir_all(parent)?;
+                    fs::create_dir_all(parent).map_err(|err| {
+                        SolcError::msg(format!(
+                            "Failed to create artifact parent folder \"{}\": {}",
+                            parent.display(),
+                            err
+                        ))
+                    })?;
                 }
                 let min = CompactContractRef::from(contract);
-                fs::write(file, serde_json::to_vec_pretty(&min)?)?
+                fs::write(&file, serde_json::to_vec_pretty(&min)?)
+                    .map_err(|err| SolcError::io(err, file))?
             }
         }
         Ok(())
     }
 
-    fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
-        let file = fs::File::open(path.as_ref())?;
-        Ok(serde_json::from_reader(file)?)
-    }
-
-    fn contract_to_artifact(contract: Contract) -> Self::Artifact {
-        CompactContract::from(contract)
+    fn contract_to_artifact(_file: &str, _name: &str, contract: Contract) -> Self::Artifact {
+        Self::Artifact::from(contract)
     }
 }
 
-/// Hardhat style artifacts
+/// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
+/// supports reading hardhat artifacts if an initial attempt to deserialize an artifact failed
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub struct HardhatArtifacts;
+pub struct MinimalCombinedArtifactsHardhatFallback;
 
-impl ArtifactOutput for HardhatArtifacts {
-    type Artifact = serde_json::Value;
+impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
+    type Artifact = CompactContract;
 
-    fn on_output(_output: &CompilerOutput, _layout: &ProjectPathsConfig) -> Result<()> {
-        todo!("Hardhat style artifacts not yet implemented")
+    fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
+        MinimalCombinedArtifacts::on_output(output, layout)
     }
 
-    fn read_cached_artifact(_path: impl AsRef<Path>) -> Result<Self::Artifact> {
-        todo!("Hardhat style artifacts not yet implemented")
+    fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
+        let path = path.as_ref();
+        let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
+        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");
+            let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
+            tracing::trace!("successfully deserialized hardhat artifact");
+            Ok(artifact.into_compact_contract())
+        }
     }
 
-    fn contract_to_artifact(_contract: Contract) -> Self::Artifact {
-        todo!("Hardhat style artifacts not yet implemented")
+    fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact {
+        MinimalCombinedArtifacts::contract_to_artifact(file, name, contract)
     }
 }
 
@@ -408,17 +457,18 @@ impl fmt::Display for AllowedLibPaths {
 }
 
 impl<T: Into<PathBuf>> TryFrom<Vec<T>> for AllowedLibPaths {
-    type Error = std::io::Error;
+    type Error = SolcIoError;
 
     fn try_from(libs: Vec<T>) -> std::result::Result<Self, Self::Error> {
         let libs = libs
             .into_iter()
             .map(|lib| {
                 let path: PathBuf = lib.into();
-                let lib = std::fs::canonicalize(path)?;
+                let lib =
+                    std::fs::canonicalize(&path).map_err(|err| SolcIoError::new(err, path))?;
                 Ok(lib)
             })
-            .collect::<std::result::Result<Vec<_>, std::io::Error>>()?;
+            .collect::<std::result::Result<Vec<_>, _>>()?;
         Ok(AllowedLibPaths(libs))
     }
 }
diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs
index 287c10a7..943c7644 100644
--- a/ethers-solc/src/error.rs
+++ b/ethers-solc/src/error.rs
@@ -1,3 +1,4 @@
+use std::{io, path::PathBuf};
 use thiserror::Error;
 
 pub type Result<T> = std::result::Result<T, SolcError>;
@@ -21,7 +22,7 @@ pub enum SolcError {
     SerdeJson(#[from] serde_json::Error),
     /// Filesystem IO error
     #[error(transparent)]
-    Io(#[from] std::io::Error),
+    Io(#[from] SolcIoError),
     #[cfg(feature = "svm")]
     #[error(transparent)]
     SvmError(#[from] svm::SolcVmError),
@@ -29,10 +30,42 @@ pub enum SolcError {
     NoContracts(String),
     #[error(transparent)]
     PatternError(#[from] glob::PatternError),
+    /// General purpose message
+    #[error("{0}")]
+    Message(String),
+
+    #[cfg(feature = "project-util")]
+    #[error(transparent)]
+    FsExtra(#[from] fs_extra::error::Error),
 }
 
 impl SolcError {
+    pub(crate) fn io(err: io::Error, path: impl Into<PathBuf>) -> Self {
+        SolcIoError::new(err, path).into()
+    }
     pub(crate) fn solc(msg: impl Into<String>) -> Self {
         SolcError::SolcError(msg.into())
     }
+    pub(crate) fn msg(msg: impl Into<String>) -> Self {
+        SolcError::Message(msg.into())
+    }
+}
+
+#[derive(Debug, Error)]
+#[error("\"{}\": {io}", self.path.display())]
+pub struct SolcIoError {
+    io: io::Error,
+    path: PathBuf,
+}
+
+impl SolcIoError {
+    pub fn new(io: io::Error, path: impl Into<PathBuf>) -> Self {
+        Self { io, path: path.into() }
+    }
+}
+
+impl From<SolcIoError> for io::Error {
+    fn from(err: SolcIoError) -> Self {
+        err.io
+    }
 }
diff --git a/ethers-solc/src/hh.rs b/ethers-solc/src/hh.rs
new file mode 100644
index 00000000..5f4a47bd
--- /dev/null
+++ b/ethers-solc/src/hh.rs
@@ -0,0 +1,130 @@
+//! Hardhat support
+
+use crate::{
+    artifacts::{BytecodeObject, CompactContract, Contract, Offsets},
+    error::{Result, SolcError},
+    ArtifactOutput, CompilerOutput, ProjectPathsConfig,
+};
+use ethers_core::abi::Abi;
+use serde::{Deserialize, Serialize};
+use std::{collections::BTreeMap, fs};
+
+const HH_ARTIFACT_VERSION: &str = "hh-sol-artifact-1";
+
+/// A hardhat artifact
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HardhatArtifact {
+    #[serde(rename = "_format")]
+    pub format: String,
+    /// A string with the contract's name.
+    pub contract_name: String,
+    /// The source name of this contract in the workspace like `contracts/Greeter.sol`
+    pub source_name: String,
+    /// The contract's ABI
+    pub abi: Abi,
+    /// A "0x"-prefixed hex string of the unlinked deployment bytecode. If the contract is not
+    /// deployable, this has the string "0x"
+    pub bytecode: BytecodeObject,
+    /// A "0x"-prefixed hex string of the unlinked runtime/deployed bytecode. If the contract is
+    /// not deployable, this has the string "0x"
+    pub deployed_bytecode: Option<BytecodeObject>,
+    /// The bytecode's link references object as returned by solc. If the contract doesn't need to
+    /// be linked, this value contains an empty object.
+    #[serde(default)]
+    pub link_references: BTreeMap<String, BTreeMap<String, Vec<Offsets>>>,
+    /// The deployed bytecode's link references object as returned by solc. If the contract doesn't
+    /// need to be linked, this value contains an empty object.
+    #[serde(default)]
+    pub deployed_link_references: BTreeMap<String, BTreeMap<String, Vec<Offsets>>>,
+}
+
+impl From<HardhatArtifact> for CompactContract {
+    fn from(artifact: HardhatArtifact) -> Self {
+        CompactContract {
+            abi: Some(artifact.abi),
+            bin: Some(artifact.bytecode),
+            bin_runtime: artifact.deployed_bytecode,
+        }
+    }
+}
+
+/// Hardhat style artifacts handler
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct HardhatArtifacts;
+
+impl ArtifactOutput for HardhatArtifacts {
+    type Artifact = HardhatArtifact;
+
+    fn on_output(output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
+        fs::create_dir_all(&layout.artifacts)
+            .map_err(|err| SolcError::msg(format!("Failed to create artifacts dir: {}", err)))?;
+        for (file, contracts) in output.contracts.iter() {
+            for (name, contract) in contracts {
+                let artifact = Self::output_file(file, name);
+                let artifact_file = layout.artifacts.join(artifact);
+                if let Some(parent) = artifact_file.parent() {
+                    fs::create_dir_all(parent).map_err(|err| {
+                        SolcError::msg(format!(
+                            "Failed to create artifact parent folder \"{}\": {}",
+                            parent.display(),
+                            err
+                        ))
+                    })?;
+                }
+                let artifact = Self::contract_to_artifact(file, name, contract.clone());
+                fs::write(&artifact_file, serde_json::to_vec_pretty(&artifact)?)
+                    .map_err(|err| SolcError::io(err, artifact_file))?
+            }
+        }
+        Ok(())
+    }
+
+    fn contract_to_artifact(file: &str, name: &str, contract: Contract) -> Self::Artifact {
+        let (bytecode, link_references, deployed_bytecode, deployed_link_references) =
+            if let Some(evm) = contract.evm {
+                let (deployed_bytecode, deployed_link_references) =
+                    if let Some(code) = evm.deployed_bytecode.and_then(|code| code.bytecode) {
+                        (Some(code.object), code.link_references)
+                    } else {
+                        (None, Default::default())
+                    };
+
+                (
+                    evm.bytecode.object,
+                    evm.bytecode.link_references,
+                    deployed_bytecode,
+                    deployed_link_references,
+                )
+            } else {
+                (Default::default(), Default::default(), None, Default::default())
+            };
+
+        HardhatArtifact {
+            format: HH_ARTIFACT_VERSION.to_string(),
+            contract_name: name.to_string(),
+            source_name: file.to_string(),
+            abi: contract.abi.unwrap_or_default(),
+            bytecode,
+            deployed_bytecode,
+            link_references,
+            deployed_link_references,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::Artifact;
+
+    #[test]
+    fn can_parse_hh_artifact() {
+        let s = include_str!("../test-data/hh-greeter-artifact.json");
+        let artifact = serde_json::from_str::<HardhatArtifact>(s).unwrap();
+        let compact = artifact.into_compact_contract();
+        assert!(compact.abi.is_some());
+        assert!(compact.bin.is_some());
+        assert!(compact.bin_runtime.is_some());
+    }
+}
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 5b23ea46..21b0edcd 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -6,6 +6,8 @@ pub use artifacts::{CompilerInput, CompilerOutput, EvmVersion};
 use std::collections::btree_map::Entry;
 
 pub mod cache;
+pub mod hh;
+pub use hh::{HardhatArtifact, HardhatArtifacts};
 
 mod compile;
 
@@ -25,13 +27,21 @@ use crate::{artifacts::Source, cache::SolFilesCache};
 pub mod error;
 pub mod utils;
 
-use crate::{artifacts::Sources, cache::PathMap};
+use crate::{
+    artifacts::Sources,
+    cache::PathMap,
+    error::{SolcError, SolcIoError},
+};
 use error::Result;
 use std::{
-    borrow::Cow, collections::BTreeMap, convert::TryInto, fmt, fs, io, marker::PhantomData,
+    borrow::Cow, collections::BTreeMap, convert::TryInto, fmt, fs, marker::PhantomData,
     path::PathBuf,
 };
 
+/// Utilities for creating, mocking and testing of (temporary) projects
+#[cfg(feature = "project-util")]
+pub mod project_util;
+
 /// Represents a project workspace and handles `solc` compiling of all contracts in that workspace.
 #[derive(Debug)]
 pub struct Project<Artifacts: ArtifactOutput = MinimalCombinedArtifacts> {
@@ -88,6 +98,21 @@ impl Project {
 }
 
 impl<Artifacts: ArtifactOutput> Project<Artifacts> {
+    /// Returns the path to the artifacts directory
+    pub fn artifacts_path(&self) -> &PathBuf {
+        &self.paths.artifacts
+    }
+
+    /// Returns the path to the sources directory
+    pub fn sources_path(&self) -> &PathBuf {
+        &self.paths.sources
+    }
+
+    /// Returns the path to the cache file
+    pub fn cache_path(&self) -> &PathBuf {
+        &self.paths.cache
+    }
+
     /// Sets the maximum number of parallel `solc` processes to run simultaneously.
     pub fn set_solc_jobs(&mut self, jobs: usize) {
         assert!(jobs > 0);
@@ -116,7 +141,7 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
 
         if let Some(cache_dir) = self.paths.cache.parent() {
             tracing::trace!("creating cache file parent directory \"{}\"", cache_dir.display());
-            fs::create_dir_all(cache_dir)?
+            fs::create_dir_all(cache_dir).map_err(|err| SolcError::io(err, cache_dir))?
         }
 
         tracing::trace!("writing cache file to \"{}\"", self.paths.cache.display());
@@ -127,9 +152,9 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
 
     /// Returns all sources found under the project's configured sources path
     #[tracing::instrument(skip_all, fields(name = "sources"))]
-    pub fn sources(&self) -> io::Result<Sources> {
+    pub fn sources(&self) -> Result<Sources> {
         tracing::trace!("reading all sources from \"{}\"", self.paths.sources.display());
-        Source::read_all_from(&self.paths.sources)
+        Ok(Source::read_all_from(&self.paths.sources)?)
     }
 
     /// This emits the cargo [`rerun-if-changed`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorerun-if-changedpath) instruction.
@@ -160,7 +185,7 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
     fn resolved_libraries(
         &self,
         sources: &Sources,
-    ) -> io::Result<BTreeMap<PathBuf, (Source, PathBuf)>> {
+    ) -> Result<BTreeMap<PathBuf, (Source, PathBuf)>> {
         let mut libs = BTreeMap::default();
         for source in sources.values() {
             for import in source.parse_imports() {
@@ -372,6 +397,7 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
         let sources = paths.set_source_names(sources);
 
         let input = CompilerInput::with_sources(sources)
+            .evm_version(self.solc_config.settings.evm_version.unwrap_or_default())
             .normalize_evm_version(&solc.version()?)
             .with_remappings(self.paths.remappings.clone());
 
@@ -460,15 +486,17 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
     }
 
     /// Removes the project's artifacts and cache file
-    pub fn cleanup(&self) -> Result<()> {
+    pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> {
         tracing::trace!("clean up project");
-        if self.paths.cache.exists() {
-            std::fs::remove_file(&self.paths.cache)?;
-            tracing::trace!("removed cache file \"{}\"", self.paths.cache.display());
+        if self.cache_path().exists() {
+            std::fs::remove_file(self.cache_path())
+                .map_err(|err| SolcIoError::new(err, self.cache_path()))?;
+            tracing::trace!("removed cache file \"{}\"", self.cache_path().display());
         }
         if self.paths.artifacts.exists() {
-            std::fs::remove_dir_all(&self.paths.artifacts)?;
-            tracing::trace!("removed artifacts dir \"{}\"", self.paths.artifacts.display());
+            std::fs::remove_dir_all(self.artifacts_path())
+                .map_err(|err| SolcIoError::new(err, self.artifacts_path().clone()))?;
+            tracing::trace!("removed artifacts dir \"{}\"", self.artifacts_path().display());
         }
         Ok(())
     }
@@ -762,14 +790,12 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
     }
 
     /// Finds the first contract with the given name and removes it from the set
-    pub fn remove(&mut self, contract: impl AsRef<str>) -> Option<T::Artifact> {
-        let contract = contract.as_ref();
+    pub fn remove(&mut self, contract_name: impl AsRef<str>) -> Option<T::Artifact> {
+        let contract_name = contract_name.as_ref();
         if let Some(output) = self.compiler_output.as_mut() {
-            if let contract @ Some(_) = output
-                .contracts
-                .values_mut()
-                .find_map(|c| c.remove(contract).map(T::contract_to_artifact))
-            {
+            if let contract @ Some(_) = output.contracts.iter_mut().find_map(|(file, c)| {
+                c.remove(contract_name).map(|c| T::contract_to_artifact(file, contract_name, c))
+            }) {
                 return contract
             }
         }
@@ -777,7 +803,7 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
             .artifacts
             .iter()
             .find_map(|(path, _)| {
-                T::contract_name(path).filter(|name| name == contract).map(|_| path)
+                T::contract_name(path).filter(|name| name == contract_name).map(|_| path)
             })?
             .clone();
         self.artifacts.remove(&key)
@@ -789,17 +815,20 @@ where
     T::Artifact: Clone,
 {
     /// Finds the first contract with the given name
-    pub fn find(&self, contract: impl AsRef<str>) -> Option<Cow<T::Artifact>> {
-        let contract = contract.as_ref();
+    pub fn find(&self, contract_name: impl AsRef<str>) -> Option<Cow<T::Artifact>> {
+        let contract_name = contract_name.as_ref();
         if let Some(output) = self.compiler_output.as_ref() {
-            if let contract @ Some(_) = output.contracts.values().find_map(|c| {
-                c.get(contract).map(|c| T::contract_to_artifact(c.clone())).map(Cow::Owned)
+            if let contract @ Some(_) = output.contracts.iter().find_map(|(file, contracts)| {
+                contracts
+                    .get(contract_name)
+                    .map(|c| T::contract_to_artifact(file, contract_name, c.clone()))
+                    .map(Cow::Owned)
             }) {
                 return contract
             }
         }
         self.artifacts.iter().find_map(|(path, art)| {
-            T::contract_name(path).filter(|name| name == contract).map(|_| Cow::Borrowed(art))
+            T::contract_name(path).filter(|name| name == contract_name).map(|_| Cow::Borrowed(art))
         })
     }
 }
diff --git a/ethers-solc/src/project_util.rs b/ethers-solc/src/project_util.rs
new file mode 100644
index 00000000..ab76cc2d
--- /dev/null
+++ b/ethers-solc/src/project_util.rs
@@ -0,0 +1,161 @@
+//! Utilities for mocking project workspaces
+use crate::{
+    config::ProjectPathsConfigBuilder,
+    error::{Result, SolcError},
+    hh::HardhatArtifacts,
+    ArtifactOutput, MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig,
+    SolcIoError,
+};
+use fs_extra::{dir, file};
+use std::path::Path;
+use tempdir::TempDir;
+
+pub struct TempProject<T: ArtifactOutput> {
+    /// temporary workspace root
+    _root: TempDir,
+    /// actual project workspace with the `root` tempdir as its root
+    inner: Project<T>,
+}
+
+impl<T: ArtifactOutput> TempProject<T> {
+    /// Makes sure all resources are created
+    fn create_new(root: TempDir, inner: Project<T>) -> std::result::Result<Self, SolcIoError> {
+        let project = Self { _root: root, inner };
+        project.paths().create_all()?;
+        Ok(project)
+    }
+
+    pub fn new(paths: ProjectPathsConfigBuilder) -> Result<Self> {
+        let tmp_dir = TempDir::new("root").map_err(|err| SolcError::io(err, "root"))?;
+        let paths = paths.build_with_root(tmp_dir.path());
+        let inner = Project::builder().artifacts().paths(paths).build()?;
+        Ok(Self::create_new(tmp_dir, inner)?)
+    }
+
+    pub fn project(&self) -> &Project<T> {
+        &self.inner
+    }
+
+    pub fn compile(&self) -> Result<ProjectCompileOutput<T>> {
+        self.project().compile()
+    }
+
+    pub fn project_mut(&mut self) -> &mut Project<T> {
+        &mut self.inner
+    }
+
+    /// The configured paths of the project
+    pub fn paths(&self) -> &ProjectPathsConfig {
+        &self.project().paths
+    }
+
+    /// The root path of the temporary workspace
+    pub fn root(&self) -> &Path {
+        self.project().paths.root.as_path()
+    }
+
+    /// Copies a single file into the projects source
+    pub fn copy_source(&self, source: impl AsRef<Path>) -> Result<()> {
+        copy_file(source, &self.paths().sources)
+    }
+
+    pub fn copy_sources<I, S>(&self, sources: I) -> Result<()>
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<Path>,
+    {
+        for path in sources {
+            self.copy_source(path)?;
+        }
+        Ok(())
+    }
+
+    /// Copies a single file into the project's main library directory
+    pub fn copy_lib(&self, lib: impl AsRef<Path>) -> Result<()> {
+        let lib_dir = self
+            .paths()
+            .libraries
+            .get(0)
+            .ok_or_else(|| SolcError::msg("No libraries folders configured"))?;
+        copy_file(lib, lib_dir)
+    }
+
+    /// Copy a series of files into the main library dir
+    pub fn copy_libs<I, S>(&self, libs: I) -> Result<()>
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<Path>,
+    {
+        for path in libs {
+            self.copy_lib(path)?;
+        }
+        Ok(())
+    }
+}
+
+impl TempProject<HardhatArtifacts> {
+    /// Creates an empty new hardhat style workspace in a new temporary dir
+    pub fn hardhat() -> Result<Self> {
+        let tmp_dir = TempDir::new("tmp_hh").map_err(|err| SolcError::io(err, "tmp_hh"))?;
+
+        let paths = ProjectPathsConfig::hardhat(tmp_dir.path())?;
+
+        let inner = Project::builder().artifacts().paths(paths).build()?;
+        Ok(Self::create_new(tmp_dir, inner)?)
+    }
+}
+
+impl TempProject<MinimalCombinedArtifacts> {
+    /// Creates an empty new dapptools style workspace in a new temporary dir
+    pub fn dapptools() -> Result<Self> {
+        let tmp_dir = TempDir::new("tmp_dapp").map_err(|err| SolcError::io(err, "temp_dapp"))?;
+        let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?;
+
+        let inner = Project::builder().artifacts().paths(paths).build()?;
+        Ok(Self::create_new(tmp_dir, inner)?)
+    }
+}
+
+impl<T: ArtifactOutput> AsRef<Project<T>> for TempProject<T> {
+    fn as_ref(&self) -> &Project<T> {
+        self.project()
+    }
+}
+
+fn dir_copy_options() -> dir::CopyOptions {
+    dir::CopyOptions {
+        overwrite: true,
+        skip_exist: false,
+        buffer_size: 64000, //64kb
+        copy_inside: true,
+        content_only: false,
+        depth: 0,
+    }
+}
+
+fn file_copy_options() -> file::CopyOptions {
+    file::CopyOptions {
+        overwrite: true,
+        skip_exist: false,
+        buffer_size: 64000, //64kb
+    }
+}
+
+/// Copies a single file into the given dir
+pub fn copy_file(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
+    let source = source.as_ref();
+    let target = target_dir.as_ref().join(
+        source
+            .file_name()
+            .ok_or_else(|| SolcError::msg(format!("No file name for {}", source.display())))?,
+    );
+
+    fs_extra::file::copy(source, target, &file_copy_options())?;
+    Ok(())
+}
+
+/// Copies all content of the source dir into the target dir
+pub fn copy_dir(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
+    fs_extra::dir::copy(source, target_dir, &dir_copy_options())?;
+    Ok(())
+}
diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs
index d5a4bd13..a7d197aa 100644
--- a/ethers-solc/src/remappings.rs
+++ b/ethers-solc/src/remappings.rs
@@ -132,16 +132,19 @@ impl Remapping {
             // nothing to find
             return Ok(Vec::new())
         }
-        let mut paths = std::fs::read_dir(path)?.into_iter().collect::<Vec<_>>();
+        let mut paths = std::fs::read_dir(path)
+            .map_err(|err| SolcError::io(err, path))?
+            .into_iter()
+            .collect::<Vec<_>>();
 
         let mut remappings = Vec::new();
-        while let Some(path) = paths.pop() {
-            let path = path?.path();
+        while let Some(p) = paths.pop() {
+            let path = p.map_err(|err| SolcError::io(err, path))?.path();
 
             // get all the directories inside a file if it's a valid dir
             if let Ok(dir) = std::fs::read_dir(&path) {
                 for inner in dir {
-                    let inner = inner?;
+                    let inner = inner.map_err(|err| SolcError::io(err, &path))?;
                     let path = inner.path().display().to_string();
                     let path = path.rsplit('/').next().unwrap().to_string();
                     if path != DAPPTOOLS_CONTRACTS_DIR && path != JS_CONTRACTS_DIR {
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index 40da5e3c..733ad6dd 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -49,17 +49,16 @@ pub fn find_version_pragma(contract: &str) -> Option<&str> {
 ///
 /// ```no_run
 /// use ethers_solc::utils;
-/// let sources = utils::source_files("./contracts").unwrap();
+/// let sources = utils::source_files("./contracts");
 /// ```
-pub fn source_files(root: impl AsRef<Path>) -> walkdir::Result<Vec<PathBuf>> {
-    let files = WalkDir::new(root)
+pub fn source_files(root: impl AsRef<Path>) -> Vec<PathBuf> {
+    WalkDir::new(root)
         .into_iter()
         .filter_map(Result::ok)
         .filter(|e| e.file_type().is_file())
         .filter(|e| e.path().extension().map(|ext| ext == "sol").unwrap_or_default())
         .map(|e| e.path().into())
-        .collect();
-    Ok(files)
+        .collect()
 }
 
 /// Returns the source name for the given source path, the ancestors of the root path
@@ -192,7 +191,7 @@ mod tests {
         File::create(&file_c).unwrap();
         File::create(&file_d).unwrap();
 
-        let files: HashSet<_> = source_files(tmp_dir.path()).unwrap().into_iter().collect();
+        let files: HashSet<_> = source_files(tmp_dir.path()).into_iter().collect();
         let expected: HashSet<_> = [file_a, file_b, file_c, file_d].into();
         assert_eq!(files, expected);
     }
diff --git a/examples/trezor.rs b/examples/trezor.rs
new file mode 100644
index 00000000..75c6b454
--- /dev/null
+++ b/examples/trezor.rs
@@ -0,0 +1,29 @@
+#[tokio::main]
+#[cfg(feature = "trezor")]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    use ethers::{prelude::*, utils::parse_ether};
+
+    // Connect over websockets
+    let provider = Provider::new(Ws::connect("ws://localhost:8545").await?);
+    // Instantiate the connection to trezor with Trezor Live derivation path and
+    // the wallet's index. You may also provide the chain_id.
+    // (here: mainnet) for EIP155 support.
+    // EIP1559 support
+    // No EIP712 support yet.
+    let trezor = Trezor::new(TrezorHDPath::TrezorLive(0), 1).await?;
+    let client = SignerMiddleware::new(provider, trezor);
+
+    // Create and broadcast a transaction (ENS disabled!)
+    // (this will require confirming the tx on the device)
+    let tx = TransactionRequest::new()
+        .to("0x99E2B13A8Ea8b00C68FA017ee250E98e870D8241")
+        .value(parse_ether(10)?);
+    let pending_tx = client.send_transaction(tx, None).await?;
+
+    // Get the receipt
+    let _receipt = pending_tx.confirmations(3).await?;
+    Ok(())
+}
+
+#[cfg(not(feature = "trezor"))]
+fn main() {}


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index b49c83bb..f3e7b4e9 100644
--- a/ethers-contract/tests/abigen.rs
+++ b/ethers-contract/tests/abigen.rs
@@ -419,3 +419,32 @@ fn can_handle_case_sensitive_calls() {
     let _ = contract.index();
     let _ = contract.INDEX();
 }
+
+#[tokio::test]
+async fn can_abiencoderv2_output() {
+    abigen!(AbiEncoderv2Test, "ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json",);
+    let ganache = ethers_core::utils::Ganache::new().spawn();
+    let from = ganache.addresses()[0];
+    let provider = Provider::try_from(ganache.endpoint())
+        .unwrap()
+        .with_sender(from)
+        .interval(std::time::Duration::from_millis(10));
+    let client = Arc::new(provider);
+
+    let contract = "AbiencoderV2Test";
+    let path = "./tests/solidity-contracts/Abiencoderv2Test.sol";
+    let compiled = Solc::default().compile_source(path).unwrap();
+    let compiled = compiled.get(path, contract).unwrap();
+    let factory = ethers_contract::ContractFactory::new(
+        compiled.abi.unwrap().clone(),
+        compiled.bytecode().unwrap().clone(),
+        client.clone(),
+    );
+    let addr = factory.deploy(()).unwrap().legacy().send().await.unwrap().address();
+
+    let contract = AbiEncoderv2Test::new(addr, client.clone());
+    let person = Person { name: "Alice".to_string(), age: 20u64.into() };
+
+    let res = contract.default_person().call().await.unwrap();
+    assert_eq!(res, person);
+}
diff --git a/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol b/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol
new file mode 100644
index 00000000..a89fcf02
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol
@@ -0,0 +1,12 @@
+pragma solidity >=0.6.0;
+pragma experimental ABIEncoderV2;
+
+contract AbiencoderV2Test {
+    struct Person {
+        string name;
+        uint age;
+    }
+    function defaultPerson() public pure returns (Person memory) {
+        return Person("Alice", 20);
+    }
+}
\ No newline at end of file
diff --git a/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json b/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json
new file mode 100644
index 00000000..13430b68
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json
@@ -0,0 +1,1 @@
+[{"inputs":[],"name":"defaultPerson","outputs":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"age","type":"uint256"}],"internalType":"struct Hello.Person","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"}]
\ No newline at end of file
diff --git a/ethers-solc/test-data/hh-greeter-artifact.json b/ethers-solc/test-data/hh-greeter-artifact.json
new file mode 100644
index 00000000..f182f4ee
--- /dev/null
+++ b/ethers-solc/test-data/hh-greeter-artifact.json
@@ -0,0 +1,48 @@
+{
+  "_format": "hh-sol-artifact-1",
+  "contractName": "Greeter",
+  "sourceName": "contracts/Greeter.sol",
+  "abi": [
+    {
+      "inputs": [
+        {
+          "internalType": "string",
+          "name": "_greeting",
+          "type": "string"
+        }
+      ],
+      "stateMutability": "nonpayable",
+      "type": "constructor"
+    },
+    {
+      "inputs": [],
+      "name": "greet",
+      "outputs": [
+        {
+          "internalType": "string",
+          "name": "",
+          "type": "string"
+        }
+      ],
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "inputs": [
+        {
+          "internalType": "string",
+          "name": "_greeting",
+          "type": "string"
+        }
+      ],
+      "name": "setGreeting",
+      "outputs": [],
+      "stateMutability": "nonpayable",
+      "type": "function"
+    }
+  ],
+  "bytecode": "0x60806040523480156200001157600080fd5b5060405162000c3238038062000c32833981810160405281019062000037919062000278565b6200006760405180606001604052806022815260200162000c1060229139826200008760201b620001ce1760201c565b80600090805190602001906200007f92919062000156565b5050620004c5565b620001298282604051602401620000a0929190620002fe565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506200012d60201b60201c565b5050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b8280546200016490620003ea565b90600052602060002090601f016020900481019282620001885760008555620001d4565b82601f10620001a357805160ff1916838001178555620001d4565b82800160010185558215620001d4579182015b82811115620001d3578251825591602001919060010190620001b6565b5b509050620001e39190620001e7565b5090565b5b8082111562000202576000816000905550600101620001e8565b5090565b60006200021d620002178462000362565b62000339565b9050828152602081018484840111156200023657600080fd5b62000243848285620003b4565b509392505050565b600082601f8301126200025d57600080fd5b81516200026f84826020860162000206565b91505092915050565b6000602082840312156200028b57600080fd5b600082015167ffffffffffffffff811115620002a657600080fd5b620002b4848285016200024b565b91505092915050565b6000620002ca8262000398565b620002d68185620003a3565b9350620002e8818560208601620003b4565b620002f381620004b4565b840191505092915050565b600060408201905081810360008301526200031a8185620002bd565b90508181036020830152620003308184620002bd565b90509392505050565b60006200034562000358565b905062000353828262000420565b919050565b6000604051905090565b600067ffffffffffffffff82111562000380576200037f62000485565b5b6200038b82620004b4565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015620003d4578082015181840152602081019050620003b7565b83811115620003e4576000848401525b50505050565b600060028204905060018216806200040357607f821691505b602082108114156200041a576200041962000456565b5b50919050565b6200042b82620004b4565b810181811067ffffffffffffffff821117156200044d576200044c62000485565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b61073b80620004d56000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a2646970667358221220d8b702fd211fe2829242f362fda35c636e2590000ac3fa2b44f0ac2fb1183cd564736f6c634300080400334465706c6f79696e67206120477265657465722077697468206772656574696e673a",
+  "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a2646970667358221220d8b702fd211fe2829242f362fda35c636e2590000ac3fa2b44f0ac2fb1183cd564736f6c63430008040033",
+  "linkReferences": {},
+  "deployedLinkReferences": {}
+}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 3157b66a..0073b388 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -1,6 +1,9 @@
 //! project tests
 
-use ethers_solc::{cache::SOLIDITY_FILES_CACHE_FILENAME, Project, ProjectPathsConfig};
+use ethers_solc::{
+    cache::SOLIDITY_FILES_CACHE_FILENAME, project_util::*, MinimalCombinedArtifacts, Project,
+    ProjectPathsConfig,
+};
 use std::{
     io,
     path::{Path, PathBuf},
@@ -9,23 +12,12 @@ use tempdir::TempDir;
 
 #[test]
 fn can_compile_hardhat_sample() {
-    let tmp_dir = TempDir::new("root").unwrap();
-    let cache = tmp_dir.path().join("cache");
-    let cache = cache.join(SOLIDITY_FILES_CACHE_FILENAME);
-    let artifacts = tmp_dir.path().join("artifacts");
-
     let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
     let paths = ProjectPathsConfig::builder()
-        .cache(cache)
         .sources(root.join("contracts"))
-        .artifacts(artifacts)
-        .lib(root.join("node_modules"))
-        .root(root)
-        .build()
-        .unwrap();
-    // let paths = ProjectPathsConfig::hardhat(root).unwrap();
+        .lib(root.join("node_modules"));
+    let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
 
-    let project = Project::builder().paths(paths).build().unwrap();
     let compiled = project.compile().unwrap();
     assert!(compiled.find("Greeter").is_some());
     assert!(compiled.find("console").is_some());
@@ -38,7 +30,7 @@ fn can_compile_hardhat_sample() {
     assert!(compiled.is_unchanged());
 
     // delete artifacts
-    std::fs::remove_dir_all(&project.paths.artifacts).unwrap();
+    std::fs::remove_dir_all(&project.paths().artifacts).unwrap();
     let compiled = project.compile().unwrap();
     assert!(compiled.find("Greeter").is_some());
     assert!(compiled.find("console").is_some());
@@ -47,22 +39,10 @@ fn can_compile_hardhat_sample() {
 
 #[test]
 fn can_compile_dapp_sample() {
-    let tmp_dir = TempDir::new("root").unwrap();
-    let cache = tmp_dir.path().join("cache");
-    let cache = cache.join(SOLIDITY_FILES_CACHE_FILENAME);
-    let artifacts = tmp_dir.path().join("out");
-
     let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
-    let paths = ProjectPathsConfig::builder()
-        .cache(cache)
-        .sources(root.join("src"))
-        .artifacts(artifacts)
-        .lib(root.join("lib"))
-        .root(root)
-        .build()
-        .unwrap();
+    let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
+    let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
 
-    let project = Project::builder().paths(paths).build().unwrap();
     let compiled = project.compile().unwrap();
     assert!(compiled.find("Dapp").is_some());
     assert!(!compiled.has_compiler_errors());
@@ -73,7 +53,7 @@ fn can_compile_dapp_sample() {
     assert!(compiled.is_unchanged());
 
     // delete artifacts
-    std::fs::remove_dir_all(&project.paths.artifacts).unwrap();
+    std::fs::remove_dir_all(&project.paths().artifacts).unwrap();
     let compiled = project.compile().unwrap();
     assert!(compiled.find("Dapp").is_some());
     assert!(!compiled.is_unchanged());
@@ -111,7 +91,7 @@ fn can_compile_dapp_sample_with_cache() {
     assert!(compiled.is_unchanged());
 
     // deleted artifacts cause recompile even with cache
-    std::fs::remove_dir_all(&project.paths.artifacts).unwrap();
+    std::fs::remove_dir_all(&project.artifacts_path()).unwrap();
     let compiled = project.compile().unwrap();
     assert!(compiled.find("Dapp").is_some());
     assert!(!compiled.is_unchanged());