Break Ethereum Deployer into crate

This commit is contained in:
Luke Parker
2024-09-15 17:13:10 -04:00
parent eb9bce6862
commit 4bcea31c2a
20 changed files with 411 additions and 74 deletions

View File

@@ -0,0 +1,34 @@
[package]
name = "serai-processor-ethereum-deployer"
version = "0.1.0"
description = "The deployer for Serai's Ethereum contracts"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/deployer"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
alloy-core = { version = "0.8", default-features = false }
alloy-consensus = { version = "0.3", default-features = false }
alloy-sol-types = { version = "0.8", default-features = false }
alloy-sol-macro = { version = "0.8", default-features = false }
alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-transport = { version = "0.3", default-features = false }
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
alloy-provider = { version = "0.3", default-features = false }
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
[build-dependencies]
build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts", default-features = false }

View File

@@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2022-2024 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,23 @@
# Ethereum Smart Contracts Deployer
The deployer for Serai's Ethereum contracts.
## Goals
It should be possible to efficiently locate the Serai Router on an blockchain with the EVM, without
relying on any centralized (or even federated) entities. While deploying and locating an instance of
the Router would be trivial, by using a fixed signature for the deployment transaction, the Router
must be constructed with the correct key for the Serai network (or set to have the correct key
post-construction). Since this cannot be guaranteed to occur, the process must be retryable and the
first successful invocation must be efficiently findable.
## Methodology
We define a contract, the Deployer, to deploy the router. This contract could use `CREATE2` with the
key representing Serai as the salt, yet this would be open to collision attacks with just 2**80
complexity. Instead, we use `CREATE` which would require 2**80 on-chain transactions (infeasible) to
use as the basis of a collision.
In order to efficiently find the contract for a key, the Deployer contract saves the addresses of
deployed contracts (indexed by the initialization code hash). This allows using a single call to a
contract with a known address to find the proper Router.

View File

@@ -0,0 +1,5 @@
fn main() {
let artifacts_path =
std::env::var("OUT_DIR").unwrap().to_string() + "/serai-processor-ethereum-deployer";
build_solidity_contracts::build(&[], "contracts", &artifacts_path).unwrap();
}

View File

@@ -0,0 +1,81 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.26;
/*
The expected deployment process of the Router is as follows:
1) A transaction deploying Deployer is made. Then, a deterministic signature is
created such that an account with an unknown private key is the creator of
the contract. Anyone can fund this address, and once anyone does, the
transaction deploying Deployer can be published by anyone. No other
transaction may be made from that account.
2) Anyone deploys the Router through the Deployer. This uses a sequential nonce
such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible.
While such attacks would still be feasible if the Deployer's address was
controllable, the usage of a deterministic signature with a NUMS method
prevents that.
This doesn't have any denial-of-service risks and will resolve once anyone steps
forward as deployer. This does fail to guarantee an identical address across
every chain, though it enables letting anyone efficiently ask the Deployer for
the address (with the Deployer having an identical address on every chain).
Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the
Deployer contract to use a consistent salt for the Router, yet the Router must
be deployed with a specific public key for Serai. Since Ethereum isn't able to
determine a valid public key (one the result of a Serai DKG) from a dishonest
public key, we have to allow multiple deployments with Serai being the one to
determine which to use.
The alternative would be to have a council publish the Serai key on-Ethereum,
with Serai verifying the published result. This would introduce a DoS risk in
the council not publishing the correct key/not publishing any key.
*/
contract Deployer {
struct Deployment {
uint64 block_number;
address created_contract;
}
mapping(bytes32 => Deployment) public deployments;
error Reentrancy();
error PriorDeployed();
error DeploymentFailed();
function deploy(bytes memory init_code) external {
// Prevent re-entrancy
// If we did allow it, one could deploy the same contract multiple times (with one overwriting
// the other's set value in storage)
bool called;
// This contract doesn't have any other use of transient storage, nor is to be inherited, making
// this usage of the zero address safe
assembly { called := tload(0) }
if (called) {
revert Reentrancy();
}
assembly { tstore(0, 1) }
// Check this wasn't prior deployed
bytes32 init_code_hash = keccak256(init_code);
Deployment memory deployment = deployments[init_code_hash];
if (deployment.created_contract == address(0)) {
revert PriorDeployed();
}
// Deploy the contract
address created_contract;
assembly {
created_contract := create(0, add(init_code, 0x20), mload(init_code))
}
if (created_contract == address(0)) {
revert DeploymentFailed();
}
// Set the dpeloyment to storage
deployment.block_number = uint64(block.number);
deployment.created_contract = created_contract;
deployments[init_code_hash] = deployment;
}
}

View File

@@ -0,0 +1,104 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::sync::Arc;
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
use alloy_consensus::{Signed, TxLegacy};
use alloy_sol_types::SolCall;
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
#[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/Deployer.sol");
}
/// The Deployer contract for the Serai Router contract.
///
/// This Deployer has a deterministic address, letting it be immediately identified on any
/// compatible chain. It then supports retrieving the Router contract's address (which isn't
/// deterministic) using a single call.
#[derive(Clone, Debug)]
pub struct Deployer;
impl Deployer {
/// Obtain the transaction to deploy this contract, already signed.
///
/// The account this transaction is sent from (which is populated in `from`) must be sufficiently
/// 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: &str =
include_str!(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 tx = TxLegacy {
chain_id: None,
nonce: 0,
// 100 gwei
gas_price: 100_000_000_000u128,
// TODO: Use a more accurate gas limit
gas_limit: 1_000_000u128,
to: TxKind::Create,
value: U256::ZERO,
input: bytecode,
};
ethereum_primitives::deterministically_sign(&tx)
}
/// Obtain the deterministic address for this contract.
pub(crate) fn address() -> Address {
let deployer_deployer =
Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
Address::create(&deployer_deployer, 0)
}
/// Construct a new view of the Deployer.
pub async fn new(
provider: Arc<RootProvider<SimpleRequest>>,
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
let address = Self::address();
let code = provider.get_code_at(address).await?;
// Contract has yet to be deployed
if code.is_empty() {
return Ok(None);
}
Ok(Some(Self))
}
/// Find the deployment of a contract.
pub async fn find_deployment(
&self,
provider: Arc<RootProvider<SimpleRequest>>,
init_code_hash: [u8; 32],
) -> Result<Option<abi::Deployer::Deployment>, RpcError<TransportErrorKind>> {
let call = TransactionRequest::default().to(Self::address()).input(TransactionInput::new(
abi::Deployer::deploymentsCall::new((init_code_hash.into(),)).abi_encode().into(),
));
let bytes = provider.call(&call).await?;
let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes, true)
.map_err(|e| {
TransportErrorKind::Custom(
format!("node returned a non-Deployment for function returning Deployment: {e:?}").into(),
)
})?
._0;
if deployment.created_contract == [0; 20] {
return Ok(None);
}
Ok(Some(deployment))
}
}