Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions bin/ev-deployer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ authors.workspace = true

[dependencies]
alloy-primitives = { workspace = true, features = ["serde"] }
alloy = { workspace = true }
alloy-rpc-types-eth = { workspace = true }
alloy-signer-local = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
toml = "0.8"
eyre = { workspace = true }
rand = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
Expand Down
75 changes: 58 additions & 17 deletions bin/ev-deployer/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! TOML config types, parsing, and validation.

use alloy_primitives::Address;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::Path};

/// Top-level deploy configuration.
#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct DeployConfig {
/// Chain configuration.
pub chain: ChainConfig,
Expand All @@ -15,14 +15,14 @@ pub(crate) struct DeployConfig {
}

/// Chain-level settings.
#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct ChainConfig {
/// The chain ID.
pub chain_id: u64,
}

/// All contract configurations.
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub(crate) struct ContractsConfig {
/// `AdminProxy` contract config (optional).
pub admin_proxy: Option<AdminProxyConfig>,
Expand All @@ -35,29 +35,33 @@ impl ContractsConfig {
fn all_addresses(&self) -> Vec<Address> {
let mut addrs = Vec::new();
if let Some(ref ap) = self.admin_proxy {
addrs.push(ap.address);
if let Some(addr) = ap.address {
addrs.push(addr);
}
}
if let Some(ref p2) = self.permit2 {
addrs.push(p2.address);
if let Some(addr) = p2.address {
addrs.push(addr);
}
}
addrs
}
}

/// `AdminProxy` configuration.
#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct AdminProxyConfig {
/// Address to deploy at.
pub address: Address,
/// Address to deploy at (required for genesis, ignored for deploy).
pub address: Option<Address>,
/// Owner address.
pub owner: Address,
}

/// `Permit2` configuration (Uniswap token approval manager).
#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct Permit2Config {
/// Address to deploy at.
pub address: Address,
/// Address to deploy at (required for genesis, ignored for deploy).
pub address: Option<Address>,
}

impl DeployConfig {
Expand All @@ -70,7 +74,7 @@ impl DeployConfig {
}

/// Validate config values.
fn validate(&self) -> eyre::Result<()> {
pub(crate) fn validate(&self) -> eyre::Result<()> {
if let Some(ref ap) = self.contracts.admin_proxy {
eyre::ensure!(
!ap.owner.is_zero(),
Expand All @@ -79,10 +83,12 @@ impl DeployConfig {
}

if let Some(ref p2) = self.contracts.permit2 {
eyre::ensure!(
!p2.address.is_zero(),
"permit2.address must not be the zero address"
);
if let Some(addr) = p2.address {
eyre::ensure!(
!addr.is_zero(),
"permit2.address must not be the zero address"
);
}
}

// Detect duplicate deploy addresses across all contracts.
Expand All @@ -93,6 +99,23 @@ impl DeployConfig {

Ok(())
}

/// Additional validation for genesis mode: all addresses must be specified.
pub(crate) fn validate_for_genesis(&self) -> eyre::Result<()> {
if let Some(ref ap) = self.contracts.admin_proxy {
eyre::ensure!(
ap.address.is_some(),
"admin_proxy.address is required for genesis mode"
);
}
if let Some(ref p2) = self.contracts.permit2 {
eyre::ensure!(
p2.address.is_some(),
"permit2.address is required for genesis mode"
);
}
Ok(())
}
}

#[cfg(test)]
Expand Down Expand Up @@ -205,6 +228,24 @@ address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
assert!(config.contracts.permit2.is_some());
}

#[test]
fn reject_missing_address_for_genesis() {
use alloy_primitives::address;

let config = DeployConfig {
chain: ChainConfig { chain_id: 1 },
contracts: ContractsConfig {
admin_proxy: Some(AdminProxyConfig {
address: None,
owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
}),
permit2: None,
},
};
config.validate().unwrap(); // base validation passes
assert!(config.validate_for_genesis().is_err());
}

#[test]
fn admin_proxy_only() {
let toml = r#"
Expand Down
8 changes: 4 additions & 4 deletions bin/ev-deployer/src/contracts/admin_proxy.rs

Large diffs are not rendered by default.

46 changes: 29 additions & 17 deletions bin/ev-deployer/src/contracts/permit2.rs

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions bin/ev-deployer/src/deploy/create2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! CREATE2 address computation.

use alloy_primitives::{keccak256, Address, Bytes, B256};

/// The deterministic deployer factory address (Nick's factory).
/// See: <https://github.com/Arachnid/deterministic-deployment-proxy>
pub(crate) const DETERMINISTIC_DEPLOYER: Address = Address::new(alloy_primitives::hex!(
"4e59b44847b379578588920ca78fbf26c0b4956c"
));

/// Compute the CREATE2 address for a contract deployed via the deterministic deployer.
///
/// The factory expects calldata `salt ++ initcode` and deploys via:
/// `CREATE2(value=0, offset, size, salt)`
///
/// The resulting address is:
/// `keccak256(0xff ++ factory ++ salt ++ keccak256(initcode))[12..]`
pub(crate) fn compute_address(salt: B256, initcode: &[u8]) -> Address {
let init_code_hash = keccak256(initcode);
DETERMINISTIC_DEPLOYER.create2(salt, init_code_hash)
}

/// Build the calldata to send to the deterministic deployer factory.
/// Format: `salt (32 bytes) ++ initcode`
pub(crate) fn build_factory_calldata(salt: B256, initcode: &[u8]) -> Bytes {
let mut data = Vec::with_capacity(32 + initcode.len());
data.extend_from_slice(salt.as_slice());
data.extend_from_slice(initcode);
Bytes::from(data)
}

#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::hex;

#[test]
fn known_create2_address() {
let salt = B256::ZERO;
// Minimal initcode: PUSH1 0x00 PUSH1 0x00 RETURN (returns empty code)
let initcode = hex!("60006000f3");
let addr = compute_address(salt, &initcode);

let init_hash = keccak256(initcode);
let expected = DETERMINISTIC_DEPLOYER.create2(salt, init_hash);
assert_eq!(addr, expected);
}

#[test]
fn different_salts_different_addresses() {
let initcode = hex!("60006000f3");
let addr1 = compute_address(B256::ZERO, &initcode);
let addr2 = compute_address(B256::with_last_byte(1), &initcode);
assert_ne!(addr1, addr2);
}

#[test]
fn different_initcode_different_addresses() {
let salt = B256::ZERO;
let addr1 = compute_address(salt, &hex!("60006000f3"));
let addr2 = compute_address(salt, &hex!("60016000f3"));
assert_ne!(addr1, addr2);
}

#[test]
fn factory_calldata_format() {
let salt = B256::with_last_byte(0x42);
let initcode = hex!("aabbcc");
let calldata = build_factory_calldata(salt, &initcode);

assert_eq!(calldata.len(), 32 + 3);
assert_eq!(&calldata[..32], salt.as_slice());
assert_eq!(&calldata[32..], &hex!("aabbcc"));
}
}
83 changes: 83 additions & 0 deletions bin/ev-deployer/src/deploy/deployer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! `ChainDeployer` trait and `LiveDeployer` implementation.

use crate::deploy::create2::{build_factory_calldata, DETERMINISTIC_DEPLOYER};
use alloy::{
network::EthereumWallet,
providers::{Provider, ProviderBuilder},
};
use alloy_primitives::{Address, Bytes, B256};
use alloy_rpc_types_eth::TransactionRequest;
use alloy_signer_local::PrivateKeySigner;
use async_trait::async_trait;

/// Receipt from a confirmed transaction.
#[derive(Debug)]
pub(crate) struct TxReceipt {
pub tx_hash: B256,
pub success: bool,
}

/// Abstracts on-chain operations for the deploy pipeline.
#[async_trait]
pub(crate) trait ChainDeployer: Send + Sync {
/// Get the chain ID of the connected chain.
async fn chain_id(&self) -> eyre::Result<u64>;

/// Read the bytecode at an address. Returns empty bytes if no code.
async fn get_code(&self, address: Address) -> eyre::Result<Bytes>;

/// Send a CREATE2 deployment transaction via the deterministic deployer.
/// Returns the tx hash once the tx is confirmed.
async fn deploy_create2(&self, salt: B256, initcode: &[u8]) -> eyre::Result<TxReceipt>;
}

/// Live deployer using alloy provider + signer.
pub(crate) struct LiveDeployer {
provider: Box<dyn Provider>,
}

impl LiveDeployer {
/// Create a new `LiveDeployer` from an RPC URL and a hex-encoded private key.
pub(crate) fn new(rpc_url: &str, private_key_hex: &str) -> eyre::Result<Self> {
let key_hex = private_key_hex
.strip_prefix("0x")
.unwrap_or(private_key_hex);
let signer: PrivateKeySigner = key_hex.parse()?;
let wallet = EthereumWallet::from(signer);

let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(rpc_url.parse()?);

Ok(Self {
provider: Box::new(provider),
})
}
}

#[async_trait]
impl ChainDeployer for LiveDeployer {
async fn chain_id(&self) -> eyre::Result<u64> {
Ok(self.provider.get_chain_id().await?)
}

async fn get_code(&self, address: Address) -> eyre::Result<Bytes> {
Ok(self.provider.get_code_at(address).await?)
}

async fn deploy_create2(&self, salt: B256, initcode: &[u8]) -> eyre::Result<TxReceipt> {
let calldata = build_factory_calldata(salt, initcode);

let tx = TransactionRequest::default()
.to(DETERMINISTIC_DEPLOYER)
.input(calldata.into());

let pending = self.provider.send_transaction(tx).await?;
let receipt = pending.get_receipt().await?;

Ok(TxReceipt {
tx_hash: receipt.transaction_hash,
success: receipt.status(),
})
}
}
4 changes: 4 additions & 0 deletions bin/ev-deployer/src/deploy/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub(crate) mod create2;
pub(crate) mod deployer;
pub(crate) mod pipeline;
pub(crate) mod state;
Loading
Loading