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;