diff --git a/processor/ethereum/router/build.rs b/processor/ethereum/router/build.rs
index 8c0fbe67..bf2cc92a 100644
--- a/processor/ethereum/router/build.rs
+++ b/processor/ethereum/router/build.rs
@@ -45,5 +45,10 @@ fn main() {
   );
 
   // Build the test contracts
-  build_solidity_contracts::build(&[], "contracts/tests", &(artifacts_path + "/tests")).unwrap();
+  build_solidity_contracts::build(
+    &["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts", "contracts"],
+    "contracts/tests",
+    &(artifacts_path + "/tests"),
+  )
+  .unwrap();
 }
diff --git a/processor/ethereum/router/contracts/Router.sol b/processor/ethereum/router/contracts/Router.sol
index 79d01226..81de35ce 100644
--- a/processor/ethereum/router/contracts/Router.sol
+++ b/processor/ethereum/router/contracts/Router.sol
@@ -414,7 +414,7 @@ contract Router is IRouterWithoutCollisions {
    *   detrimental to other `OutInstruction`s within the same batch) is sufficiently concerning to
    *   justify this.
    */
-  function createAddress(uint256 nonce) private view returns (address) {
+  function createAddress(uint256 nonce) internal view returns (address) {
     unchecked {
       /*
         The hashed RLP-encoding is:
@@ -438,9 +438,10 @@ contract Router is IRouterWithoutCollisions {
           bitsNeeded += 8;
         }
         uint256 bytesNeeded = bitsNeeded / 8;
-        rlpEncodingLen = 22 + bytesNeeded;
+        // 22 + 1 + the amount of bytes needed
+        rlpEncodingLen = 23 + bytesNeeded;
         // Shift from byte 31 to byte 22
-        rlpEncoding |= 0x80 + (bytesNeeded << 72);
+        rlpEncoding |= (0x80 + bytesNeeded) << 72;
         // Shift past the unnecessary bytes
         rlpEncoding |= nonce << (72 - bitsNeeded);
       }
diff --git a/processor/ethereum/router/contracts/tests/CreateAddress.sol b/processor/ethereum/router/contracts/tests/CreateAddress.sol
new file mode 100644
index 00000000..6be58fe2
--- /dev/null
+++ b/processor/ethereum/router/contracts/tests/CreateAddress.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity ^0.8.26;
+
+import "Router.sol";
+
+// Wrap the Router with a contract which exposes the address
+contract CreateAddress is Router {
+  constructor() Router(bytes32(uint256(1))) {}
+
+  function createAddressForSelf(uint256 nonce) external returns (address) {
+    return Router.createAddress(nonce);
+  }
+}
diff --git a/processor/ethereum/router/src/tests/create_address.rs b/processor/ethereum/router/src/tests/create_address.rs
new file mode 100644
index 00000000..a431e5e1
--- /dev/null
+++ b/processor/ethereum/router/src/tests/create_address.rs
@@ -0,0 +1,97 @@
+use alloy_core::primitives::{hex, U256, Bytes, TxKind};
+use alloy_sol_types::SolCall;
+
+use alloy_consensus::TxLegacy;
+
+use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
+use alloy_provider::Provider;
+
+use revm::{primitives::SpecId, interpreter::gas::calculate_initial_tx_gas};
+
+use crate::tests::Test;
+
+#[rustfmt::skip]
+#[expect(warnings)]
+#[expect(needless_pass_by_value)]
+#[expect(clippy::all)]
+#[expect(clippy::ignored_unit_patterns)]
+#[expect(clippy::redundant_closure_for_method_calls)]
+mod abi {
+  alloy_sol_macro::sol!("contracts/tests/CreateAddress.sol");
+}
+
+#[tokio::test]
+async fn test_create_address() {
+  let test = Test::new().await;
+
+  let address = {
+    const BYTECODE: &[u8] = {
+      const BYTECODE_HEX: &[u8] = include_bytes!(concat!(
+        env!("OUT_DIR"),
+        "/serai-processor-ethereum-router/tests/CreateAddress.bin"
+      ));
+      const BYTECODE: [u8; BYTECODE_HEX.len() / 2] =
+        match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
+          Ok(bytecode) => bytecode,
+          Err(_) => panic!("CreateAddress.bin did not contain valid hex"),
+        };
+      &BYTECODE
+    };
+
+    let tx = TxLegacy {
+      chain_id: None,
+      nonce: 0,
+      gas_price: 100_000_000_000u128,
+      gas_limit: 1_100_000,
+      to: TxKind::Create,
+      value: U256::ZERO,
+      input: Bytes::from_static(BYTECODE),
+    };
+    let tx = ethereum_primitives::deterministically_sign(tx);
+    let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
+    receipt.contract_address.unwrap()
+  };
+
+  // Check `createAddress` correctly encodes the nonce for every single meaningful bit pattern
+  // The only meaningful patterns are < 0x80, == 0x80, and then each length greater > 0x80
+  // The following covers all three
+  let mut nonce = 1u64;
+  while nonce.checked_add(nonce).is_some() {
+    assert_eq!(
+      &test
+        .provider
+        .call(
+          &TransactionRequest::default().to(address).input(TransactionInput::new(
+            (abi::CreateAddress::createAddressForSelfCall { nonce: U256::from(nonce) })
+              .abi_encode()
+              .into()
+          ))
+        )
+        .await
+        .unwrap()
+        .as_ref()[12 ..],
+      address.create(nonce).as_slice(),
+    );
+    nonce <<= 1;
+  }
+
+  let input =
+    (abi::CreateAddress::createAddressForSelfCall { nonce: U256::from(u64::MAX) }).abi_encode();
+  let gas = test
+    .provider
+    .estimate_gas(
+      &TransactionRequest::default().to(address).input(TransactionInput::new(input.clone().into())),
+    )
+    .await
+    .unwrap() -
+    calculate_initial_tx_gas(SpecId::CANCUN, &input, false, &[], 0).initial_gas;
+
+  let keccak256_gas_estimate = |len: u64| 30 + (6 * len.div_ceil(32));
+  let mut bytecode_len = 0;
+  while (keccak256_gas_estimate(bytecode_len) + keccak256_gas_estimate(85)) < gas {
+    bytecode_len += 32;
+  }
+  println!(
+    "Worst-case createAddress gas: {gas}, CREATE2 break-even is bytecode of length {bytecode_len}",
+  );
+}
diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs
index 61572e6e..5937df3b 100644
--- a/processor/ethereum/router/src/tests/mod.rs
+++ b/processor/ethereum/router/src/tests/mod.rs
@@ -41,6 +41,9 @@ use crate::{
 };
 
 mod constants;
+
+mod create_address;
+
 mod erc20;
 use erc20::Erc20;