From 3c9c12d3203c46ad4748b96a0bb86ed8fac63e5b Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 18 Jan 2025 23:58:38 -0500 Subject: [PATCH] Test the Deployer contract --- Cargo.lock | 4 + processor/ethereum/deployer/Cargo.toml | 8 ++ processor/ethereum/deployer/src/lib.rs | 52 ++++++++--- processor/ethereum/deployer/src/tests.rs | 107 +++++++++++++++++++++++ 4 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 processor/ethereum/deployer/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 4d6bf218..f28b9701 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6926,14 +6926,18 @@ version = "0.1.0" dependencies = [ "alloy-consensus", "alloy-core", + "alloy-node-bindings", "alloy-provider", + "alloy-rpc-client", "alloy-rpc-types-eth", "alloy-simple-request-transport", "alloy-sol-macro", "alloy-sol-types", "alloy-transport", "build-solidity-contracts", + "serai-ethereum-test-primitives", "serai-processor-ethereum-primitives", + "tokio", ] [[package]] diff --git a/processor/ethereum/deployer/Cargo.toml b/processor/ethereum/deployer/Cargo.toml index 1b8e191e..3e0f7d5b 100644 --- a/processor/ethereum/deployer/Cargo.toml +++ b/processor/ethereum/deployer/Cargo.toml @@ -33,3 +33,11 @@ ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = [build-dependencies] build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts", default-features = false } + +[dev-dependencies] +alloy-rpc-client = { version = "0.9", default-features = false } +alloy-node-bindings = { version = "0.9", default-features = false } + +tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] } + +ethereum-test-primitives = { package = "serai-ethereum-test-primitives", path = "../test-primitives" } diff --git a/processor/ethereum/deployer/src/lib.rs b/processor/ethereum/deployer/src/lib.rs index a4d6ed94..f810d617 100644 --- a/processor/ethereum/deployer/src/lib.rs +++ b/processor/ethereum/deployer/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; -use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind}; +use alloy_core::primitives::{hex, Address, U256, Bytes, TxKind}; use alloy_consensus::{Signed, TxLegacy}; use alloy_sol_types::SolCall; @@ -14,6 +14,9 @@ use alloy_transport::{TransportErrorKind, RpcError}; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; +#[cfg(test)] +mod tests; + #[rustfmt::skip] #[expect(warnings)] #[expect(needless_pass_by_value)] @@ -24,6 +27,17 @@ mod abi { alloy_sol_macro::sol!("contracts/Deployer.sol"); } +const BYTECODE: &[u8] = { + const BYTECODE_HEX: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-deployer/Deployer.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!("Deployer.bin did not contain valid hex"), + }; + &BYTECODE +}; + /// The Deployer contract for the Serai Router contract. /// /// This Deployer has a deterministic address, letting it be immediately identified on any instance @@ -38,21 +52,39 @@ impl Deployer { /// funded for this transaction to be submitted. This account has no known private key to anyone /// so ETH sent can be neither misappropriated nor returned. pub fn deployment_tx() -> Signed { - pub const BYTECODE: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-deployer/Deployer.bin")); - let bytecode = - Bytes::from_hex(BYTECODE).expect("compiled-in Deployer bytecode wasn't valid hex"); + let bytecode = Bytes::from(BYTECODE); // Legacy transactions are used to ensure the widest possible degree of support across EVMs let tx = TxLegacy { chain_id: None, nonce: 0, - // This uses a fixed gas price as necessary to achieve a deterministic address - // The gas price is fixed to 100 gwei, which should be incredibly generous, in order to make - // this getting stuck unlikely. While expensive, this only has to occur once + /* + This needs to use a fixed gas price to achieve a deterministic address. The gas price is + fixed to 100 gwei, which should be generous, in order to make this unlikely to get stuck. + While potentially expensive, this only has to occur per chain this is deployed on. + + If this is too low of a gas price, private mempools can be used, with other transactions in + the bundle raising the gas price to acceptable levels. While this strategy could be + entirely relied upon, allowing the gas price paid to reflect the network's actual gas + price, that wouldn't work for EVM networks without private mempools. + + That leaves this as failing only if it violates a protocol constant, or if the gas price is + too low on a network without private mempools to publish via. In that case, this code + should to be forked to accept an enum of which network the deployment is for (with the gas + price derivative of that, as common as possible across networks to minimize the amount of + addresses representing the Deployer). + */ gas_price: 100_000_000_000u128, - // TODO: Use a more accurate gas limit - gas_limit: 1_000_000u64, + /* + This is twice the cost of deployment as of Ethereum's Cancun upgrade. The wide margin is to + increase the likelihood of surviving changes to the cost of contract deployment (notably + the gas cost of calldata). While wasteful, this only has to be done once per chain and is + accepted accordingly. + + If this is ever unacceptable, the parameterization suggested in case the `gas_price` is + unacceptable should be implemented. + */ + gas_limit: 300_698, to: TxKind::Create, value: U256::ZERO, input: bytecode, diff --git a/processor/ethereum/deployer/src/tests.rs b/processor/ethereum/deployer/src/tests.rs new file mode 100644 index 00000000..ba1e75ae --- /dev/null +++ b/processor/ethereum/deployer/src/tests.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; +use alloy_simple_request_transport::SimpleRequest; +use alloy_rpc_client::ClientBuilder; +use alloy_provider::{Provider, RootProvider}; + +use alloy_node_bindings::Anvil; + +use crate::{ + abi::Deployer::{PriorDeployed, DeploymentFailed, DeployerErrors}, + Deployer, +}; + +#[tokio::test] +async fn test_deployer() { + const CANCUN: &str = "cancun"; + const LATEST: &str = "latest"; + + for network in [CANCUN, LATEST] { + let anvil = Anvil::new().arg("--hardfork").arg(network).spawn(); + + let provider = Arc::new(RootProvider::new( + ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true), + )); + + // Deploy the Deployer + { + let deployment_tx = Deployer::deployment_tx(); + let gas_programmed = deployment_tx.tx().gas_limit; + let receipt = ethereum_test_primitives::publish_tx(&provider, deployment_tx).await; + assert!(receipt.status()); + assert_eq!(receipt.contract_address.unwrap(), Deployer::address()); + + if network == CANCUN { + // Check the gas programmed was twice the gas used + // We only check this for cancun as the constant was programmed per cancun's gas pricing + assert_eq!(2 * receipt.gas_used, gas_programmed); + } + } + + // Deploy the deployer with the deployer + let mut deploy_tx = Deployer::deploy_tx(crate::BYTECODE.to_vec()); + deploy_tx.gas_price = 100_000_000_000u128; + deploy_tx.gas_limit = 1_000_000; + { + let deploy_tx = ethereum_primitives::deterministically_sign(deploy_tx.clone()); + let receipt = ethereum_test_primitives::publish_tx(&provider, deploy_tx).await; + assert!(receipt.status()); + } + + // Verify we can now find the deployer + { + let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap(); + let deployed_deployer = deployer + .find_deployment(ethereum_primitives::keccak256(crate::BYTECODE)) + .await + .unwrap() + .unwrap(); + assert_eq!( + provider.get_code_at(deployed_deployer).await.unwrap(), + provider.get_code_at(Deployer::address()).await.unwrap(), + ); + assert!(deployed_deployer != Deployer::address()); + } + + // Verify deploying the same init code multiple times fails + { + let mut deploy_tx = deploy_tx; + // Change the gas price to cause a distinct message, and with it, a distinct signer + deploy_tx.gas_price += 1; + let deploy_tx = ethereum_primitives::deterministically_sign(deploy_tx); + let receipt = ethereum_test_primitives::publish_tx(&provider, deploy_tx.clone()).await; + assert!(!receipt.status()); + + let call = TransactionRequest::default() + .to(Deployer::address()) + .input(TransactionInput::new(deploy_tx.tx().input.clone())); + let call_err = provider.call(&call).await.unwrap_err(); + assert!(matches!( + call_err.as_error_resp().unwrap().as_decoded_error::(true).unwrap(), + DeployerErrors::PriorDeployed(PriorDeployed {}), + )); + } + + // Verify deployment failures yield errors properly + { + // 0xfe is an invalid opcode which is guaranteed to remain invalid + let mut deploy_tx = Deployer::deploy_tx(vec![0xfe]); + deploy_tx.gas_price = 100_000_000_000u128; + deploy_tx.gas_limit = 1_000_000; + + let deploy_tx = ethereum_primitives::deterministically_sign(deploy_tx); + let receipt = ethereum_test_primitives::publish_tx(&provider, deploy_tx.clone()).await; + assert!(!receipt.status()); + + let call = TransactionRequest::default() + .to(Deployer::address()) + .input(TransactionInput::new(deploy_tx.tx().input.clone())); + let call_err = provider.call(&call).await.unwrap_err(); + assert!(matches!( + call_err.as_error_resp().unwrap().as_decoded_error::(true).unwrap(), + DeployerErrors::DeploymentFailed(DeploymentFailed {}), + )); + } + } +}