From 835b5bb06f280ae1a9bc8900eecc4e48d52b7659 Mon Sep 17 00:00:00 2001
From: Luke Parker <lukeparker5132@gmail.com>
Date: Mon, 27 Jan 2025 13:59:11 -0500
Subject: [PATCH] Split tests across a few files, fuzz generate OutInstructions

Tests successful gas estimation even with more complex behaviors.
---
 .../ethereum/router/src/tests/escape_hatch.rs | 172 +++++++
 .../router/src/tests/in_instruction.rs        | 182 ++++++++
 processor/ethereum/router/src/tests/mod.rs    | 421 ++++--------------
 3 files changed, 435 insertions(+), 340 deletions(-)
 create mode 100644 processor/ethereum/router/src/tests/escape_hatch.rs
 create mode 100644 processor/ethereum/router/src/tests/in_instruction.rs

diff --git a/processor/ethereum/router/src/tests/escape_hatch.rs b/processor/ethereum/router/src/tests/escape_hatch.rs
new file mode 100644
index 00000000..28be1a64
--- /dev/null
+++ b/processor/ethereum/router/src/tests/escape_hatch.rs
@@ -0,0 +1,172 @@
+use alloy_core::primitives::{Address, U256};
+
+use alloy_consensus::TxLegacy;
+
+use alloy_provider::Provider;
+
+use crate::tests::*;
+
+impl Test {
+  pub(crate) fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
+    let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
+    let sig = sign(self.state.key.unwrap(), &msg);
+    let mut tx = self.router.escape_hatch(escape_to, &sig);
+    tx.gas_limit = Router::ESCAPE_HATCH_GAS + 5_000;
+    tx
+  }
+
+  pub(crate) async fn escape_hatch(&mut self) {
+    let mut escape_to = [0; 20];
+    OsRng.fill_bytes(&mut escape_to);
+    let escape_to = Address(escape_to.into());
+
+    // Set the code of the address to escape to so it isn't flagged as a non-contract
+    let () = self.provider.raw_request("anvil_setCode".into(), (escape_to, [0])).await.unwrap();
+
+    let mut tx = self.escape_hatch_tx(escape_to);
+    tx.gas_price = 100_000_000_000;
+    let tx = ethereum_primitives::deterministically_sign(tx);
+    let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
+    assert!(receipt.status());
+    // This encodes an address which has 12 bytes of padding
+    assert_eq!(
+      CalldataAgnosticGas::calculate(tx.tx().input.as_ref(), 12, receipt.gas_used),
+      Router::ESCAPE_HATCH_GAS
+    );
+
+    {
+      let block = receipt.block_number.unwrap();
+      let executed = self.router.executed(block ..= block).await.unwrap();
+      assert_eq!(executed.len(), 1);
+      assert_eq!(executed[0], Executed::EscapeHatch { nonce: self.state.next_nonce, escape_to });
+    }
+
+    self.state.next_nonce += 1;
+    self.state.escaped_to = Some(escape_to);
+    self.verify_state().await;
+  }
+
+  pub(crate) fn escape_tx(&self, coin: Coin) -> TxLegacy {
+    let mut tx = self.router.escape(coin);
+    tx.gas_limit = 100_000;
+    tx.gas_price = 100_000_000_000;
+    tx
+  }
+}
+
+#[tokio::test]
+async fn test_escape_hatch() {
+  let mut test = Test::new().await;
+  test.confirm_next_serai_key().await;
+
+  // Queue another key so the below test cases can run
+  test.update_serai_key().await;
+
+  {
+    // The zero address should be invalid to escape to
+    assert!(matches!(
+      test.call_and_decode_err(test.escape_hatch_tx([0; 20].into())).await,
+      IRouterErrors::InvalidEscapeAddress(IRouter::InvalidEscapeAddress {})
+    ));
+    // Empty addresses should be invalid to escape to
+    assert!(matches!(
+      test.call_and_decode_err(test.escape_hatch_tx([1; 20].into())).await,
+      IRouterErrors::EscapeAddressWasNotAContract(IRouter::EscapeAddressWasNotAContract {})
+    ));
+    // Non-empty addresses without code should be invalid to escape to
+    let tx = ethereum_primitives::deterministically_sign(TxLegacy {
+      to: Address([1; 20].into()).into(),
+      gas_limit: 21_000,
+      gas_price: 100_000_000_000,
+      value: U256::from(1),
+      ..Default::default()
+    });
+    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
+    assert!(receipt.status());
+    assert!(matches!(
+      test.call_and_decode_err(test.escape_hatch_tx([1; 20].into())).await,
+      IRouterErrors::EscapeAddressWasNotAContract(IRouter::EscapeAddressWasNotAContract {})
+    ));
+
+    // Escaping at this point in time should fail
+    assert!(matches!(
+      test.call_and_decode_err(test.router.escape(Coin::Ether)).await,
+      IRouterErrors::EscapeHatchNotInvoked(IRouter::EscapeHatchNotInvoked {})
+    ));
+  }
+
+  // Invoke the escape hatch
+  test.escape_hatch().await;
+
+  // Now that the escape hatch has been invoked, all of the following calls should fail
+  {
+    assert!(matches!(
+      test.call_and_decode_err(test.update_serai_key_tx().1).await,
+      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
+    ));
+    assert!(matches!(
+      test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
+      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
+    ));
+    assert!(matches!(
+      test.call_and_decode_err(test.eth_in_instruction_tx().3).await,
+      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
+    ));
+    assert!(matches!(
+      test
+        .call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), [].as_slice().into()).1)
+        .await,
+      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
+    ));
+    // We reject further attempts to update the escape hatch to prevent the last key from being
+    // able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such
+    // as after the validators represented unstake)
+    assert!(matches!(
+      test.call_and_decode_err(test.escape_hatch_tx(test.state.escaped_to.unwrap())).await,
+      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
+    ));
+  }
+
+  // Check the escape fn itself
+
+  // ETH
+  {
+    let () = test
+      .provider
+      .raw_request("anvil_setBalance".into(), (test.router.address(), 1))
+      .await
+      .unwrap();
+    let tx = ethereum_primitives::deterministically_sign(test.escape_tx(Coin::Ether));
+    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
+    assert!(receipt.status());
+
+    let block = receipt.block_number.unwrap();
+    assert_eq!(
+      test.router.escapes(block ..= block).await.unwrap(),
+      vec![Escape { coin: Coin::Ether, amount: U256::from(1) }],
+    );
+
+    assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
+    assert_eq!(
+      test.provider.get_balance(test.state.escaped_to.unwrap()).await.unwrap(),
+      U256::from(1)
+    );
+  }
+
+  // ERC20
+  {
+    let erc20 = Erc20::deploy(&test).await;
+    let coin = Coin::Erc20(erc20.address());
+    let amount = U256::from(1);
+    erc20.mint(&test, test.router.address(), amount).await;
+
+    let tx = ethereum_primitives::deterministically_sign(test.escape_tx(coin));
+    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
+    assert!(receipt.status());
+
+    let block = receipt.block_number.unwrap();
+    assert_eq!(test.router.escapes(block ..= block).await.unwrap(), vec![Escape { coin, amount }],);
+    assert_eq!(erc20.balance_of(&test, test.router.address()).await, U256::from(0));
+    assert_eq!(erc20.balance_of(&test, test.state.escaped_to.unwrap()).await, amount);
+  }
+}
diff --git a/processor/ethereum/router/src/tests/in_instruction.rs b/processor/ethereum/router/src/tests/in_instruction.rs
new file mode 100644
index 00000000..20ddfd02
--- /dev/null
+++ b/processor/ethereum/router/src/tests/in_instruction.rs
@@ -0,0 +1,182 @@
+use std::collections::HashSet;
+
+use alloy_core::primitives::U256;
+use alloy_sol_types::SolCall;
+
+use alloy_consensus::{TxLegacy, Signed};
+
+use scale::Encode;
+use serai_client::{
+  primitives::SeraiAddress,
+  in_instructions::primitives::{
+    InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand,
+  },
+};
+
+use ethereum_primitives::LogIndex;
+
+use crate::{InInstruction, tests::*};
+
+impl Test {
+  pub(crate) fn in_instruction() -> Shorthand {
+    Shorthand::Raw(RefundableInInstruction {
+      origin: None,
+      instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
+    })
+  }
+
+  pub(crate) fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) {
+    let coin = Coin::Ether;
+    let amount = U256::from(1);
+    let shorthand = Self::in_instruction();
+
+    let mut tx = self.router.in_instruction(coin, amount, &shorthand);
+    tx.gas_limit = 1_000_000;
+    tx.gas_price = 100_000_000_000;
+
+    (coin, amount, shorthand, tx)
+  }
+
+  pub(crate) async fn publish_in_instruction_tx(
+    &self,
+    tx: Signed<TxLegacy>,
+    coin: Coin,
+    amount: U256,
+    shorthand: &Shorthand,
+  ) {
+    let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
+    assert!(receipt.status());
+
+    let block = receipt.block_number.unwrap();
+
+    if matches!(coin, Coin::Erc20(_)) {
+      // If we don't whitelist this token, we shouldn't be yielded an InInstruction
+      let in_instructions =
+        self.router.in_instructions_unordered(block ..= block, &HashSet::new()).await.unwrap();
+      assert!(in_instructions.is_empty());
+    }
+
+    let in_instructions = self
+      .router
+      .in_instructions_unordered(
+        block ..= block,
+        &if let Coin::Erc20(token) = coin { HashSet::from([token]) } else { HashSet::new() },
+      )
+      .await
+      .unwrap();
+    assert_eq!(in_instructions.len(), 1);
+
+    let in_instruction_log_index = receipt.inner.logs().iter().find_map(|log| {
+      (log.topics().first() == Some(&crate::InInstructionEvent::SIGNATURE_HASH))
+        .then(|| log.log_index.unwrap())
+    });
+    // If this isn't an InInstruction event, it'll be a top-level transfer event
+    let log_index = in_instruction_log_index.unwrap_or(0);
+
+    assert_eq!(
+      in_instructions[0],
+      InInstruction {
+        id: LogIndex { block_hash: *receipt.block_hash.unwrap(), index_within_block: log_index },
+        transaction_hash: **tx.hash(),
+        from: tx.recover_signer().unwrap(),
+        coin,
+        amount,
+        data: shorthand.encode(),
+      }
+    );
+  }
+}
+
+#[tokio::test]
+async fn test_no_in_instruction_before_key() {
+  let test = Test::new().await;
+
+  // We shouldn't be able to publish `InInstruction`s before publishing a key
+  let (_coin, _amount, _shorthand, tx) = test.eth_in_instruction_tx();
+  assert!(matches!(
+    test.call_and_decode_err(tx).await,
+    IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
+  ));
+}
+
+#[tokio::test]
+async fn test_eth_in_instruction() {
+  let mut test = Test::new().await;
+  test.confirm_next_serai_key().await;
+
+  let (coin, amount, shorthand, tx) = test.eth_in_instruction_tx();
+
+  // This should fail if the value mismatches the amount
+  {
+    let mut tx = tx.clone();
+    tx.value = U256::ZERO;
+    assert!(matches!(
+      test.call_and_decode_err(tx).await,
+      IRouterErrors::AmountMismatchesMsgValue(IRouter::AmountMismatchesMsgValue {})
+    ));
+  }
+
+  let tx = ethereum_primitives::deterministically_sign(tx);
+  test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
+}
+
+#[tokio::test]
+async fn test_erc20_router_in_instruction() {
+  let mut test = Test::new().await;
+  test.confirm_next_serai_key().await;
+
+  let erc20 = Erc20::deploy(&test).await;
+
+  let coin = Coin::Erc20(erc20.address());
+  let amount = U256::from(1);
+  let shorthand = Test::in_instruction();
+
+  // The provided `in_instruction` function will use a top-level transfer for ERC20 InInstructions,
+  // so we have to manually write this call
+  let tx = TxLegacy {
+    chain_id: None,
+    nonce: 0,
+    gas_price: 100_000_000_000,
+    gas_limit: 1_000_000,
+    to: test.router.address().into(),
+    value: U256::ZERO,
+    input: crate::abi::inInstructionCall::new((coin.into(), amount, shorthand.encode().into()))
+      .abi_encode()
+      .into(),
+  };
+
+  // If no `approve` was granted, this should fail
+  assert!(matches!(
+    test.call_and_decode_err(tx.clone()).await,
+    IRouterErrors::TransferFromFailed(IRouter::TransferFromFailed {})
+  ));
+
+  let tx = ethereum_primitives::deterministically_sign(tx);
+  {
+    let signer = tx.recover_signer().unwrap();
+    erc20.mint(&test, signer, amount).await;
+    erc20.approve(&test, signer, test.router.address(), amount).await;
+  }
+
+  test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
+}
+
+#[tokio::test]
+async fn test_erc20_top_level_transfer_in_instruction() {
+  let mut test = Test::new().await;
+  test.confirm_next_serai_key().await;
+
+  let erc20 = Erc20::deploy(&test).await;
+
+  let coin = Coin::Erc20(erc20.address());
+  let amount = U256::from(1);
+  let shorthand = Test::in_instruction();
+
+  let mut tx = test.router.in_instruction(coin, amount, &shorthand);
+  tx.gas_price = 100_000_000_000;
+  tx.gas_limit = 1_000_000;
+
+  let tx = ethereum_primitives::deterministically_sign(tx);
+  erc20.mint(&test, tx.recover_signer().unwrap(), amount).await;
+  test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
+}
diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs
index 2f6397cc..bc086c62 100644
--- a/processor/ethereum/router/src/tests/mod.rs
+++ b/processor/ethereum/router/src/tests/mod.rs
@@ -1,4 +1,4 @@
-use std::{sync::Arc, collections::HashSet};
+use std::sync::Arc;
 
 use rand_core::{RngCore, OsRng};
 
@@ -20,16 +20,8 @@ use alloy_provider::{
 
 use alloy_node_bindings::{Anvil, AnvilInstance};
 
-use scale::Encode;
-use serai_client::{
-  networks::ethereum::{ContractDeployment, Address as SeraiEthereumAddress},
-  primitives::SeraiAddress,
-  in_instructions::primitives::{
-    InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand,
-  },
-};
+use serai_client::networks::ethereum::{ContractDeployment, Address as SeraiEthereumAddress};
 
-use ethereum_primitives::LogIndex;
 use ethereum_schnorr::{PublicKey, Signature};
 use ethereum_deployer::Deployer;
 
@@ -37,16 +29,18 @@ use crate::{
   _irouter_abi::IRouterWithoutCollisions::{
     self as IRouter, IRouterWithoutCollisionsErrors as IRouterErrors,
   },
-  Coin, InInstruction, OutInstructions, Router, Executed, Escape,
+  Coin, OutInstructions, Router, Executed, Escape,
 };
 
 mod constants;
 
-mod create_address;
-
 mod erc20;
 use erc20::Erc20;
 
+mod create_address;
+mod in_instruction;
+mod escape_hatch;
+
 pub(crate) fn test_key() -> (Scalar, PublicKey) {
   loop {
     let key = Scalar::random(&mut OsRng);
@@ -126,7 +120,13 @@ impl Test {
 
   async fn new() -> Self {
     // The following is explicitly only evaluated against the cancun network upgrade at this time
-    let anvil = Anvil::new().arg("--hardfork").arg("cancun").arg("--tracing").spawn();
+    let anvil = Anvil::new()
+      .arg("--hardfork")
+      .arg("cancun")
+      .arg("--tracing")
+      .arg("--no-request-size-limit")
+      .arg("--disable-block-gas-limit")
+      .spawn();
 
     let provider = Arc::new(RootProvider::new(
       ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
@@ -267,74 +267,6 @@ impl Test {
     self.verify_state().await;
   }
 
-  fn in_instruction() -> Shorthand {
-    Shorthand::Raw(RefundableInInstruction {
-      origin: None,
-      instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
-    })
-  }
-
-  fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) {
-    let coin = Coin::Ether;
-    let amount = U256::from(1);
-    let shorthand = Self::in_instruction();
-
-    let mut tx = self.router.in_instruction(coin, amount, &shorthand);
-    tx.gas_limit = 1_000_000;
-    tx.gas_price = 100_000_000_000;
-
-    (coin, amount, shorthand, tx)
-  }
-
-  async fn publish_in_instruction_tx(
-    &self,
-    tx: Signed<TxLegacy>,
-    coin: Coin,
-    amount: U256,
-    shorthand: &Shorthand,
-  ) {
-    let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
-    assert!(receipt.status());
-
-    let block = receipt.block_number.unwrap();
-
-    if matches!(coin, Coin::Erc20(_)) {
-      // If we don't whitelist this token, we shouldn't be yielded an InInstruction
-      let in_instructions =
-        self.router.in_instructions_unordered(block ..= block, &HashSet::new()).await.unwrap();
-      assert!(in_instructions.is_empty());
-    }
-
-    let in_instructions = self
-      .router
-      .in_instructions_unordered(
-        block ..= block,
-        &if let Coin::Erc20(token) = coin { HashSet::from([token]) } else { HashSet::new() },
-      )
-      .await
-      .unwrap();
-    assert_eq!(in_instructions.len(), 1);
-
-    let in_instruction_log_index = receipt.inner.logs().iter().find_map(|log| {
-      (log.topics().first() == Some(&crate::InInstructionEvent::SIGNATURE_HASH))
-        .then(|| log.log_index.unwrap())
-    });
-    // If this isn't an InInstruction event, it'll be a top-level transfer event
-    let log_index = in_instruction_log_index.unwrap_or(0);
-
-    assert_eq!(
-      in_instructions[0],
-      InInstruction {
-        id: LogIndex { block_hash: *receipt.block_hash.unwrap(), index_within_block: log_index },
-        transaction_hash: **tx.hash(),
-        from: tx.recover_signer().unwrap(),
-        coin,
-        amount,
-        data: shorthand.encode(),
-      }
-    );
-  }
-
   fn execute_tx(
     &self,
     coin: Coin,
@@ -371,7 +303,7 @@ impl Test {
     results: Vec<bool>,
   ) -> (Signed<TxLegacy>, u64) {
     let (message_hash, mut tx) = self.execute_tx(coin, fee, out_instructions);
-    tx.gas_limit = 1_000_000;
+    tx.gas_limit = 100_000_000;
     tx.gas_price = 100_000_000_000;
     let tx = ethereum_primitives::deterministically_sign(tx);
     let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
@@ -396,52 +328,6 @@ impl Test {
     (tx.clone(), receipt.gas_used)
   }
 
-  fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
-    let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
-    let sig = sign(self.state.key.unwrap(), &msg);
-    let mut tx = self.router.escape_hatch(escape_to, &sig);
-    tx.gas_limit = Router::ESCAPE_HATCH_GAS + 5_000;
-    tx
-  }
-
-  async fn escape_hatch(&mut self) {
-    let mut escape_to = [0; 20];
-    OsRng.fill_bytes(&mut escape_to);
-    let escape_to = Address(escape_to.into());
-
-    // Set the code of the address to escape to so it isn't flagged as a non-contract
-    let () = self.provider.raw_request("anvil_setCode".into(), (escape_to, [0])).await.unwrap();
-
-    let mut tx = self.escape_hatch_tx(escape_to);
-    tx.gas_price = 100_000_000_000;
-    let tx = ethereum_primitives::deterministically_sign(tx);
-    let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
-    assert!(receipt.status());
-    // This encodes an address which has 12 bytes of padding
-    assert_eq!(
-      CalldataAgnosticGas::calculate(tx.tx().input.as_ref(), 12, receipt.gas_used),
-      Router::ESCAPE_HATCH_GAS
-    );
-
-    {
-      let block = receipt.block_number.unwrap();
-      let executed = self.router.executed(block ..= block).await.unwrap();
-      assert_eq!(executed.len(), 1);
-      assert_eq!(executed[0], Executed::EscapeHatch { nonce: self.state.next_nonce, escape_to });
-    }
-
-    self.state.next_nonce += 1;
-    self.state.escaped_to = Some(escape_to);
-    self.verify_state().await;
-  }
-
-  fn escape_tx(&self, coin: Coin) -> TxLegacy {
-    let mut tx = self.router.escape(coin);
-    tx.gas_limit = 100_000;
-    tx.gas_price = 100_000_000_000;
-    tx
-  }
-
   async fn gas_unused_by_calls(&self, tx: &Signed<TxLegacy>) -> u64 {
     let mut unused_gas = 0;
 
@@ -612,100 +498,6 @@ async fn test_update_serai_key() {
   test.confirm_next_serai_key().await;
 }
 
-#[tokio::test]
-async fn test_no_in_instruction_before_key() {
-  let test = Test::new().await;
-
-  // We shouldn't be able to publish `InInstruction`s before publishing a key
-  let (_coin, _amount, _shorthand, tx) = test.eth_in_instruction_tx();
-  assert!(matches!(
-    test.call_and_decode_err(tx).await,
-    IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
-  ));
-}
-
-#[tokio::test]
-async fn test_eth_in_instruction() {
-  let mut test = Test::new().await;
-  test.confirm_next_serai_key().await;
-
-  let (coin, amount, shorthand, tx) = test.eth_in_instruction_tx();
-
-  // This should fail if the value mismatches the amount
-  {
-    let mut tx = tx.clone();
-    tx.value = U256::ZERO;
-    assert!(matches!(
-      test.call_and_decode_err(tx).await,
-      IRouterErrors::AmountMismatchesMsgValue(IRouter::AmountMismatchesMsgValue {})
-    ));
-  }
-
-  let tx = ethereum_primitives::deterministically_sign(tx);
-  test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
-}
-
-#[tokio::test]
-async fn test_erc20_router_in_instruction() {
-  let mut test = Test::new().await;
-  test.confirm_next_serai_key().await;
-
-  let erc20 = Erc20::deploy(&test).await;
-
-  let coin = Coin::Erc20(erc20.address());
-  let amount = U256::from(1);
-  let shorthand = Test::in_instruction();
-
-  // The provided `in_instruction` function will use a top-level transfer for ERC20 InInstructions,
-  // so we have to manually write this call
-  let tx = TxLegacy {
-    chain_id: None,
-    nonce: 0,
-    gas_price: 100_000_000_000,
-    gas_limit: 1_000_000,
-    to: test.router.address().into(),
-    value: U256::ZERO,
-    input: crate::abi::inInstructionCall::new((coin.into(), amount, shorthand.encode().into()))
-      .abi_encode()
-      .into(),
-  };
-
-  // If no `approve` was granted, this should fail
-  assert!(matches!(
-    test.call_and_decode_err(tx.clone()).await,
-    IRouterErrors::TransferFromFailed(IRouter::TransferFromFailed {})
-  ));
-
-  let tx = ethereum_primitives::deterministically_sign(tx);
-  {
-    let signer = tx.recover_signer().unwrap();
-    erc20.mint(&test, signer, amount).await;
-    erc20.approve(&test, signer, test.router.address(), amount).await;
-  }
-
-  test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
-}
-
-#[tokio::test]
-async fn test_erc20_top_level_transfer_in_instruction() {
-  let mut test = Test::new().await;
-  test.confirm_next_serai_key().await;
-
-  let erc20 = Erc20::deploy(&test).await;
-
-  let coin = Coin::Erc20(erc20.address());
-  let amount = U256::from(1);
-  let shorthand = Test::in_instruction();
-
-  let mut tx = test.router.in_instruction(coin, amount, &shorthand);
-  tx.gas_price = 100_000_000_000;
-  tx.gas_limit = 1_000_000;
-
-  let tx = ethereum_primitives::deterministically_sign(tx);
-  erc20.mint(&test, tx.recover_signer().unwrap(), amount).await;
-  test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
-}
-
 #[tokio::test]
 async fn test_execute_arbitrary_code() {
   let test = Test::new().await;
@@ -966,123 +758,6 @@ async fn test_result_decoding() {
   assert!(gas_used <= gas);
 }
 
-#[tokio::test]
-async fn test_escape_hatch() {
-  let mut test = Test::new().await;
-  test.confirm_next_serai_key().await;
-
-  // Queue another key so the below test cases can run
-  test.update_serai_key().await;
-
-  {
-    // The zero address should be invalid to escape to
-    assert!(matches!(
-      test.call_and_decode_err(test.escape_hatch_tx([0; 20].into())).await,
-      IRouterErrors::InvalidEscapeAddress(IRouter::InvalidEscapeAddress {})
-    ));
-    // Empty addresses should be invalid to escape to
-    assert!(matches!(
-      test.call_and_decode_err(test.escape_hatch_tx([1; 20].into())).await,
-      IRouterErrors::EscapeAddressWasNotAContract(IRouter::EscapeAddressWasNotAContract {})
-    ));
-    // Non-empty addresses without code should be invalid to escape to
-    let tx = ethereum_primitives::deterministically_sign(TxLegacy {
-      to: Address([1; 20].into()).into(),
-      gas_limit: 21_000,
-      gas_price: 100_000_000_000,
-      value: U256::from(1),
-      ..Default::default()
-    });
-    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
-    assert!(receipt.status());
-    assert!(matches!(
-      test.call_and_decode_err(test.escape_hatch_tx([1; 20].into())).await,
-      IRouterErrors::EscapeAddressWasNotAContract(IRouter::EscapeAddressWasNotAContract {})
-    ));
-
-    // Escaping at this point in time should fail
-    assert!(matches!(
-      test.call_and_decode_err(test.router.escape(Coin::Ether)).await,
-      IRouterErrors::EscapeHatchNotInvoked(IRouter::EscapeHatchNotInvoked {})
-    ));
-  }
-
-  // Invoke the escape hatch
-  test.escape_hatch().await;
-
-  // Now that the escape hatch has been invoked, all of the following calls should fail
-  {
-    assert!(matches!(
-      test.call_and_decode_err(test.update_serai_key_tx().1).await,
-      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
-    ));
-    assert!(matches!(
-      test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
-      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
-    ));
-    assert!(matches!(
-      test.call_and_decode_err(test.eth_in_instruction_tx().3).await,
-      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
-    ));
-    assert!(matches!(
-      test
-        .call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), [].as_slice().into()).1)
-        .await,
-      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
-    ));
-    // We reject further attempts to update the escape hatch to prevent the last key from being
-    // able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such
-    // as after the validators represented unstake)
-    assert!(matches!(
-      test.call_and_decode_err(test.escape_hatch_tx(test.state.escaped_to.unwrap())).await,
-      IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
-    ));
-  }
-
-  // Check the escape fn itself
-
-  // ETH
-  {
-    let () = test
-      .provider
-      .raw_request("anvil_setBalance".into(), (test.router.address(), 1))
-      .await
-      .unwrap();
-    let tx = ethereum_primitives::deterministically_sign(test.escape_tx(Coin::Ether));
-    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
-    assert!(receipt.status());
-
-    let block = receipt.block_number.unwrap();
-    assert_eq!(
-      test.router.escapes(block ..= block).await.unwrap(),
-      vec![Escape { coin: Coin::Ether, amount: U256::from(1) }],
-    );
-
-    assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
-    assert_eq!(
-      test.provider.get_balance(test.state.escaped_to.unwrap()).await.unwrap(),
-      U256::from(1)
-    );
-  }
-
-  // ERC20
-  {
-    let erc20 = Erc20::deploy(&test).await;
-    let coin = Coin::Erc20(erc20.address());
-    let amount = U256::from(1);
-    erc20.mint(&test, test.router.address(), amount).await;
-
-    let tx = ethereum_primitives::deterministically_sign(test.escape_tx(coin));
-    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
-    assert!(receipt.status());
-
-    let block = receipt.block_number.unwrap();
-    assert_eq!(test.router.escapes(block ..= block).await.unwrap(), vec![Escape { coin, amount }],);
-    assert_eq!(erc20.balance_of(&test, test.router.address()).await, U256::from(0));
-    assert_eq!(erc20.balance_of(&test, test.state.escaped_to.unwrap()).await, amount);
-  }
-}
-
 #[tokio::test]
 async fn test_reentrancy() {
   let mut test = Test::new().await;
@@ -1121,3 +796,69 @@ async fn test_reentrancy() {
   // (which this isn't a counter-example to) and is validated to be the worst-case, but is peculiar
   assert!(gas_used <= gas);
 }
+
+#[tokio::test]
+async fn fuzz_test_out_instructions_gas() {
+  for _ in 0 .. 10 {
+    let mut test = Test::new().await;
+    test.confirm_next_serai_key().await;
+
+    // Generate a random OutInstructions
+    let mut out_instructions = vec![];
+    let mut prior_addresses = vec![];
+    for _ in 0 .. (OsRng.next_u64() % 50) {
+      let amount_out = U256::from(OsRng.next_u64() % 2);
+      if (OsRng.next_u64() % 2) == 1 {
+        let mut code = return_true_code();
+
+        // Extend this with random data to make it somewhat random, despite the constant returned
+        // code (though the estimator will never run the initcode and realize that)
+        let ext = vec![0; usize::try_from(OsRng.next_u64() % 400).unwrap()];
+        code.extend(&ext);
+
+        out_instructions.push((
+          SeraiEthereumAddress::Contract(ContractDeployment::new(100_000, ext).unwrap()),
+          amount_out,
+        ));
+      } else {
+        // Occasionally reuse addresses (cold/warm slots)
+        let address = if (!prior_addresses.is_empty()) && ((OsRng.next_u64() % 2) == 1) {
+          prior_addresses[usize::try_from(
+            OsRng.next_u64() % u64::try_from(prior_addresses.len()).unwrap(),
+          )
+          .unwrap()]
+        } else {
+          let mut rand_address = [0; 20];
+          OsRng.fill_bytes(&mut rand_address);
+          prior_addresses.push(rand_address);
+          rand_address
+        };
+        out_instructions.push((SeraiEthereumAddress::Address(address), amount_out));
+      }
+    }
+    let out_instructions = OutInstructions::from(out_instructions.as_slice());
+
+    // Randomly decide the coin
+    let coin = if (OsRng.next_u64() % 2) == 1 {
+      let () = test
+        .provider
+        .raw_request("anvil_setBalance".into(), (test.router.address(), 1_000_000_000))
+        .await
+        .unwrap();
+      Coin::Ether
+    } else {
+      let erc20 = Erc20::deploy(&test).await;
+      erc20.mint(&test, test.router.address(), U256::from(1_000_000_000)).await;
+      Coin::Erc20(erc20.address())
+    };
+
+    let fee_per_gas = U256::from(OsRng.next_u64() % 10);
+    let gas = test.router.execute_gas(coin, fee_per_gas, &out_instructions);
+    let fee = U256::from(gas) * fee_per_gas;
+    // All of these should have succeeded
+    let (tx, gas_used) =
+      test.execute(coin, fee, out_instructions.clone(), vec![true; out_instructions.0.len()]).await;
+    let unused_gas = test.gas_unused_by_calls(&tx).await;
+    assert_eq!(gas_used + unused_gas, gas);
+  }
+}