Repository: gakonst/ethers-rs
Pull request #1647 (2022-08-30 13:25:28)

fix: set chain id explicitly
<!--
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
anvil now enforces chain id https://github.com/foundry-rs/foundry/pull/2995, whereas ganache does not.
this broke the tests

hardhat also enforces correct chain id, https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md 

implemented [_validateTxV](https://github.com/ethereumjs/ethereumjs-monorepo/blob/2d60261422437c2353e5b547dbde610494d37c5e/packages/tx/src/legacyTransaction.ts#L361)

added test to confirm that

<!--
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
* set chain id where required
* expose `Anvil::chain_id`

it'd be nice if the `Anvil` instance could just return a Wallet directly, but this is currently not possible since `Wallet` is not part of the core crate...
perhaps moving  + feature gating would be possible, but a bit of a hassle...

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


mattsse: remaining errors seem to be limited to etherscan

Patch:
diff --git a/ethers-core/src/utils/anvil.rs b/ethers-core/src/utils/anvil.rs
index ac941b5b..05cfabed 100644
--- a/ethers-core/src/utils/anvil.rs
+++ b/ethers-core/src/utils/anvil.rs
@@ -1,5 +1,5 @@
 use crate::{
-    types::Address,
+    types::{Address, Chain},
     utils::{secret_key_to_address, unused_port},
 };
 use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
@@ -21,6 +21,7 @@ pub struct AnvilInstance {
     private_keys: Vec<K256SecretKey>,
     addresses: Vec<Address>,
     port: u16,
+    chain_id: Option<u64>,
 }
 
 impl AnvilInstance {
@@ -39,6 +40,11 @@ impl AnvilInstance {
         self.port
     }
 
+    /// Returns the chain of the anvil instance
+    pub fn chain_id(&self) -> u64 {
+        self.chain_id.unwrap_or_else(|| Chain::AnvilHardhat.into())
+    }
+
     /// Returns the HTTP endpoint of this instance
     pub fn endpoint(&self) -> String {
         format!("http://localhost:{}", self.port)
@@ -82,6 +88,7 @@ pub struct Anvil {
     program: Option<PathBuf>,
     port: Option<u16>,
     block_time: Option<u64>,
+    chain_id: Option<u64>,
     mnemonic: Option<String>,
     fork: Option<String>,
     fork_block_number: Option<u64>,
@@ -139,6 +146,13 @@ impl Anvil {
         self
     }
 
+    /// Sets the chain_id the `anvil` instance will use.
+    #[must_use]
+    pub fn chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
+        self.chain_id = Some(chain_id.into());
+        self
+    }
+
     /// Sets the mnemonic which will be used when the `anvil` instance is launched.
     #[must_use]
     pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
@@ -208,6 +222,10 @@ impl Anvil {
             cmd.arg("-m").arg(mnemonic);
         }
 
+        if let Some(chain_id) = self.chain_id {
+            cmd.arg("--chain-id").arg(chain_id.to_string());
+        }
+
         if let Some(block_time) = self.block_time {
             cmd.arg("-b").arg(block_time.to_string());
         }
@@ -258,7 +276,7 @@ impl Anvil {
 
         child.stdout = Some(reader.into_inner());
 
-        AnvilInstance { pid: child, private_keys, addresses, port }
+        AnvilInstance { pid: child, private_keys, addresses, port, chain_id: self.chain_id }
     }
 }
 
diff --git a/examples/contract_human_readable.rs b/examples/contract_human_readable.rs
index 33ddc2d2..3148dc58 100644
--- a/examples/contract_human_readable.rs
+++ b/examples/contract_human_readable.rs
@@ -42,7 +42,7 @@ async fn main() -> Result<()> {
         Provider::<Http>::try_from(anvil.endpoint())?.interval(Duration::from_millis(10u64));
 
     // 4. instantiate the client with the wallet
-    let client = SignerMiddleware::new(provider, wallet);
+    let client = SignerMiddleware::new(provider, wallet.with_chain_id(anvil.chain_id()));
     let client = Arc::new(client);
 
     // 5. create a factory which will be used to deploy instances of the contract
diff --git a/examples/contract_with_abi.rs b/examples/contract_with_abi.rs
index 67219a78..35425777 100644
--- a/examples/contract_with_abi.rs
+++ b/examples/contract_with_abi.rs
@@ -31,7 +31,7 @@ async fn main() -> Result<()> {
         Provider::<Http>::try_from(anvil.endpoint())?.interval(Duration::from_millis(10u64));
 
     // 4. instantiate the client with the wallet
-    let client = SignerMiddleware::new(provider, wallet);
+    let client = SignerMiddleware::new(provider, wallet.with_chain_id(anvil.chain_id()));
     let client = Arc::new(client);
 
     // 5. create a factory which will be used to deploy instances of the contract
diff --git a/examples/contract_with_abi_and_bytecode.rs b/examples/contract_with_abi_and_bytecode.rs
index c1920b12..2748e252 100644
--- a/examples/contract_with_abi_and_bytecode.rs
+++ b/examples/contract_with_abi_and_bytecode.rs
@@ -22,7 +22,7 @@ async fn main() -> Result<()> {
         Provider::<Http>::try_from(anvil.endpoint())?.interval(Duration::from_millis(10u64));
 
     // 4. instantiate the client with the wallet
-    let client = Arc::new(SignerMiddleware::new(provider, wallet));
+    let client = Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(anvil.chain_id())));
 
     // 5. deploy contract
     let greeter_contract =
diff --git a/examples/local_signer.rs b/examples/local_signer.rs
index 64c570c3..764de4f5 100644
--- a/examples/local_signer.rs
+++ b/examples/local_signer.rs
@@ -13,7 +13,7 @@ async fn main() -> Result<()> {
     let provider = Provider::<Http>::try_from(anvil.endpoint())?;
 
     // connect the wallet to the provider
-    let client = SignerMiddleware::new(provider, wallet);
+    let client = SignerMiddleware::new(provider, wallet.with_chain_id(anvil.chain_id()));
 
     // craft the transaction
     let tx = TransactionRequest::new().to(wallet2.address()).value(10000);


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index 15658294..273498d0 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -4,7 +4,7 @@
 use ethers_contract::{abigen, Abigen, EthCall, EthEvent};
 use ethers_core::{
     abi::{AbiDecode, AbiEncode, Address, Tokenizable},
-    types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256},
+    types::{transaction::eip2718::TypedTransaction, Chain, Eip1559TransactionRequest, U256},
     utils::Anvil,
 };
 use ethers_middleware::SignerMiddleware;
@@ -608,7 +608,8 @@ async fn can_send_struct_param() {
     let server = Anvil::new().spawn();
     let wallet: LocalWallet = server.keys()[0].clone().into();
     let provider = Provider::try_from(server.endpoint()).unwrap();
-    let client = Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(1337u64)));
+    let client =
+        Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(Chain::AnvilHardhat)));
 
     let contract = StructContract::deploy(client, ()).unwrap().legacy().send().await.unwrap();
 
diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs
index 0f1e94ff..3929e7c5 100644
--- a/ethers-middleware/tests/signer.rs
+++ b/ethers-middleware/tests/signer.rs
@@ -50,6 +50,40 @@ async fn send_eth() {
     assert!(balance_before > balance_after);
 }
 
+// hardhat compatibility test, to show hardhat rejects tx signed for other chains
+#[tokio::test]
+#[cfg(not(feature = "celo"))]
+#[ignore]
+async fn send_with_chain_id_hardhat() {
+    let wallet: LocalWallet =
+        "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse().unwrap();
+    let provider = Provider::try_from("http://localhost:8545").unwrap();
+    let client = SignerMiddleware::new(provider, wallet);
+
+    let tx = TransactionRequest::new().to(Address::random()).value(100u64);
+    let res = client.send_transaction(tx, None).await;
+
+    let err = res.unwrap_err();
+    assert!(err
+        .to_string()
+        .contains("Trying to send an incompatible EIP-155 transaction, signed for another chain."));
+}
+
+#[tokio::test]
+#[cfg(not(feature = "celo"))]
+#[ignore]
+async fn send_with_chain_id_anvil() {
+    let wallet: LocalWallet =
+        "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse().unwrap();
+    let provider = Provider::try_from("http://localhost:8545").unwrap();
+    let client = SignerMiddleware::new(provider, wallet);
+
+    let tx = TransactionRequest::new().to(Address::random()).value(100u64);
+    let res = client.send_transaction(tx, None).await;
+
+    let _err = res.unwrap_err();
+}
+
 #[tokio::test]
 #[cfg(not(feature = "celo"))]
 async fn pending_txs_with_confirmations_testnet() {
diff --git a/examples/ethers-wasm/tests/contract_with_abi.rs b/examples/ethers-wasm/tests/contract_with_abi.rs
index 678f54b2..5530df67 100644
--- a/examples/ethers-wasm/tests/contract_with_abi.rs
+++ b/examples/ethers-wasm/tests/contract_with_abi.rs
@@ -1,12 +1,14 @@
 #![cfg(target_arch = "wasm32")]
 
-use wasm_bindgen_test::*;
-
-use ethers::prelude::{
-    abigen, ContractFactory, Http, JsonRpcClient, LocalWallet, Provider, SignerMiddleware, Ws,
+use ethers::{
+    prelude::{
+        abigen, ContractFactory, Http, JsonRpcClient, LocalWallet, Provider, SignerMiddleware, Ws,
+    },
+    signers::Signer,
+    types::Chain,
 };
-
 use std::{convert::TryFrom, sync::Arc};
+use wasm_bindgen_test::*;
 
 wasm_bindgen_test_configure!(run_in_browser);
 
@@ -22,14 +24,14 @@ abigen!(
 async fn http_connect_and_deploy() {
     console_log!("connecting http...");
     let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
-    deploy(provider, ethers_wasm::utils::key(0)).await;
+    deploy(provider, ethers_wasm::utils::key(0).with_chain_id(Chain::AnvilHardhat)).await;
 }
 
 #[wasm_bindgen_test]
 async fn ws_connect_and_deploy() {
     console_log!("connecting ws...");
     let provider = Provider::new(Ws::connect("ws://localhost:8545").await.unwrap());
-    deploy(provider, ethers_wasm::utils::key(1)).await;
+    deploy(provider, ethers_wasm::utils::key(1).with_chain_id(Chain::AnvilHardhat)).await;
 }
 
 async fn deploy<T: JsonRpcClient>(provider: Provider<T>, wallet: LocalWallet) {