mirror of
https://github.com/serai-dex/serai.git
synced 2025-04-22 22:18:15 +00:00
Test the Deployer contract
This commit is contained in:
parent
f6b52b3fd3
commit
3c9c12d320
4 changed files with 161 additions and 10 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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<TxLegacy> {
|
||||
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,
|
||||
|
|
107
processor/ethereum/deployer/src/tests.rs
Normal file
107
processor/ethereum/deployer/src/tests.rs
Normal file
|
@ -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::<DeployerErrors>(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::<DeployerErrors>(true).unwrap(),
|
||||
DeployerErrors::DeploymentFailed(DeploymentFailed {}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue