diff --git a/.github/workflows/ev_deployer.yml b/.github/workflows/ev_deployer.yml new file mode 100644 index 00000000..ef57c12a --- /dev/null +++ b/.github/workflows/ev_deployer.yml @@ -0,0 +1,79 @@ +name: EV Deployer CI + +on: + push: + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ev_deployer.yml' + - 'contracts/src/**' + - 'contracts/foundry.toml' + - 'bin/ev-deployer/**' + pull_request: + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ev_deployer.yml' + - 'contracts/src/**' + - 'contracts/foundry.toml' + - 'bin/ev-deployer/**' + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + verify-bytecodes: + name: EV Deployer bytecode verification + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run bytecode verification tests + run: cargo test -p ev-deployer -- --ignored --test-threads=1 + + unit-tests: + name: EV Deployer unit tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Run unit tests + run: cargo test -p ev-deployer + + e2e-genesis: + name: EV Deployer e2e genesis test + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run e2e genesis test + run: bash bin/ev-deployer/tests/e2e_genesis.sh diff --git a/Cargo.lock b/Cargo.lock index 2c4de487..3e41035d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2917,6 +2917,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "ev-deployer" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "clap", + "eyre", + "serde", + "serde_json", + "tempfile", + "toml 0.8.23", +] + [[package]] name = "ev-dev" version = "0.1.0" @@ -5848,7 +5861,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.5+spec-1.1.0", ] [[package]] @@ -6595,7 +6608,7 @@ dependencies = [ "tar", "tokio", "tokio-stream", - "toml", + "toml 0.9.12+spec-1.1.0", "tracing", "url", "zstd", @@ -6673,7 +6686,7 @@ dependencies = [ "reth-stages-types", "reth-static-file-types", "serde", - "toml", + "toml 0.9.12+spec-1.1.0", "url", ] @@ -8004,7 +8017,7 @@ dependencies = [ "shellexpand", "strum", "thiserror 2.0.18", - "toml", + "toml 0.9.12+spec-1.1.0", "tracing", "url", "vergen", @@ -9804,6 +9817,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -10460,6 +10482,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -10468,13 +10502,22 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned", + "serde_spanned 1.0.4", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -10493,6 +10536,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + [[package]] name = "toml_edit" version = "0.25.5+spec-1.1.0" @@ -10514,6 +10571,12 @@ dependencies = [ "winnow 1.0.0", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.7+spec-1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3a3d8245..70c3486e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "bin/ev-deployer", "bin/ev-dev", "bin/ev-reth", "crates/common", diff --git a/bin/ev-deployer/Cargo.toml b/bin/ev-deployer/Cargo.toml new file mode 100644 index 00000000..b80d21a8 --- /dev/null +++ b/bin/ev-deployer/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ev-deployer" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["serde"] } +clap = { workspace = true, features = ["derive", "env"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +toml = "0.8" +eyre = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/bin/ev-deployer/README.md b/bin/ev-deployer/README.md new file mode 100644 index 00000000..58af3a3e --- /dev/null +++ b/bin/ev-deployer/README.md @@ -0,0 +1,134 @@ +# EV Deployer + +CLI tool for generating genesis alloc entries for ev-reth contracts. It reads a declarative TOML config and produces the JSON needed to embed contracts into a chain's genesis state. + +## Building + +```bash +just build-deployer +``` + +The binary is output to `target/release/ev-deployer`. + +## Configuration + +EV Deployer uses a TOML config file to define what contracts to include and how to configure them. See [`examples/devnet.toml`](examples/devnet.toml) for a complete example. + +```toml +[chain] +chain_id = 1234 + +[contracts.admin_proxy] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[contracts.fee_vault] +address = "0x000000000000000000000000000000000000FE00" +owner = "0x000000000000000000000000000000000000Ad00" +destination_domain = 0 +recipient_address = "0x0000000000000000000000000000000000000000000000000000000000000000" +minimum_amount = 0 +call_fee = 0 +bridge_share_bps = 10000 +other_recipient = "0x0000000000000000000000000000000000000000" +hyp_native_minter = "0x0000000000000000000000000000000000000000" +``` + +Both contracts are optional — include only the sections you need. + +### Config reference + +#### `[chain]` + +| Field | Type | Description | +|------------|------|-------------| +| `chain_id` | u64 | Chain ID | + +#### `[contracts.admin_proxy]` + +| Field | Type | Description | +|-----------|---------|---------------------------| +| `address` | address | Address to deploy at | +| `owner` | address | Owner (must not be zero) | + +#### `[contracts.fee_vault]` + +| Field | Type | Default | Description | +|----------------------|---------|---------|------------------------------------------------| +| `address` | address | — | Address to deploy at | +| `owner` | address | — | Owner (must not be zero) | +| `destination_domain` | u32 | 0 | Hyperlane destination domain | +| `recipient_address` | bytes32 | 0x0…0 | Hyperlane recipient | +| `minimum_amount` | u64 | 0 | Minimum amount for bridging | +| `call_fee` | u64 | 0 | Fee for sendToCelestia | +| `bridge_share_bps` | u64 | 0 | Bridge share in basis points (0–10000). 0 maps to 10000 | +| `other_recipient` | address | 0x0…0 | Split accounting recipient | +| `hyp_native_minter` | address | 0x0…0 | HypNativeMinter address | + +## Usage + +### Generate genesis alloc + +Print alloc JSON to stdout: + +```bash +ev-deployer genesis --config deploy.toml +``` + +Write to a file: + +```bash +ev-deployer genesis --config deploy.toml --output alloc.json +``` + +### Merge into an existing genesis file + +Insert the generated entries into an existing `genesis.json`. The merged result is written to `--output` (or stdout if `--output` is omitted): + +```bash +ev-deployer genesis --config deploy.toml --merge-into genesis.json --output genesis-out.json +``` + +If an address already exists in the genesis, the command fails. Use `--force` to overwrite: + +```bash +ev-deployer genesis --config deploy.toml --merge-into genesis.json --output genesis-out.json --force +``` + +### Export address manifest + +Write a JSON mapping of contract names to their configured addresses: + +```bash +ev-deployer genesis --config deploy.toml --addresses-out addresses.json +``` + +Output: + +```json +{ + "admin_proxy": "0x000000000000000000000000000000000000Ad00", + "fee_vault": "0x000000000000000000000000000000000000FE00" +} +``` + +### Look up a contract address + +```bash +ev-deployer compute-address --config deploy.toml --contract admin_proxy +``` + +## Contracts + +| Contract | Description | +|----------------|-----------------------------------------------------| +| `admin_proxy` | Proxy contract with owner-based access control | +| `fee_vault` | Fee vault with Hyperlane bridge integration | + +Runtime bytecodes are embedded in the binary — no external toolchain is needed at deploy time. + +## Testing + +```bash +just test-deployer +``` diff --git a/bin/ev-deployer/examples/devnet.toml b/bin/ev-deployer/examples/devnet.toml new file mode 100644 index 00000000..f332b1ad --- /dev/null +++ b/bin/ev-deployer/examples/devnet.toml @@ -0,0 +1,17 @@ +[chain] +chain_id = 1234 + +[contracts.admin_proxy] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[contracts.fee_vault] +address = "0x000000000000000000000000000000000000FE00" +owner = "0x000000000000000000000000000000000000Ad00" +destination_domain = 0 +recipient_address = "0x0000000000000000000000000000000000000000000000000000000000000000" +minimum_amount = 0 +call_fee = 0 +bridge_share_bps = 10000 +other_recipient = "0x0000000000000000000000000000000000000000" +hyp_native_minter = "0x0000000000000000000000000000000000000000" diff --git a/bin/ev-deployer/src/config.rs b/bin/ev-deployer/src/config.rs new file mode 100644 index 00000000..66fb1c24 --- /dev/null +++ b/bin/ev-deployer/src/config.rs @@ -0,0 +1,208 @@ +//! TOML config types, parsing, and validation. + +use alloy_primitives::{Address, B256}; +use serde::Deserialize; +use std::path::Path; + +/// Top-level deploy configuration. +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub(crate) struct DeployConfig { + /// Chain configuration. + pub chain: ChainConfig, + /// Contract configurations. + pub contracts: ContractsConfig, +} + +/// Chain-level settings. +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub(crate) struct ChainConfig { + /// The chain ID. + pub chain_id: u64, +} + +/// All contract configurations. +#[derive(Debug, Deserialize)] +pub(crate) struct ContractsConfig { + /// `AdminProxy` contract config (optional). + pub admin_proxy: Option, + /// `FeeVault` contract config (optional). + pub fee_vault: Option, +} + +/// `AdminProxy` configuration. +#[derive(Debug, Deserialize)] +pub(crate) struct AdminProxyConfig { + /// Address to deploy at. + pub address: Address, + /// Owner address. + pub owner: Address, +} + +/// `FeeVault` configuration. +#[derive(Debug, Deserialize)] +pub(crate) struct FeeVaultConfig { + /// Address to deploy at. + pub address: Address, + /// Owner address. + pub owner: Address, + /// Hyperlane destination domain. + #[serde(default)] + pub destination_domain: u32, + /// Hyperlane recipient address (bytes32). + #[serde(default)] + pub recipient_address: B256, + /// Minimum amount for bridging. + #[serde(default)] + pub minimum_amount: u64, + /// Call fee for sendToCelestia. + #[serde(default)] + pub call_fee: u64, + /// Basis points for bridge share (0-10000). 0 defaults to 10000. + #[serde(default)] + pub bridge_share_bps: u64, + /// Other recipient for split accounting. + #[serde(default)] + pub other_recipient: Address, + /// `HypNativeMinter` address. + #[serde(default)] + pub hyp_native_minter: Address, +} + +impl DeployConfig { + /// Load and validate config from a TOML file. + pub(crate) fn load(path: &Path) -> eyre::Result { + let content = std::fs::read_to_string(path)?; + let config: Self = toml::from_str(&content)?; + config.validate()?; + Ok(config) + } + + /// Validate config values. + fn validate(&self) -> eyre::Result<()> { + if let Some(ref ap) = self.contracts.admin_proxy { + eyre::ensure!( + !ap.owner.is_zero(), + "admin_proxy.owner must not be the zero address" + ); + } + + if let Some(ref fv) = self.contracts.fee_vault { + eyre::ensure!( + !fv.owner.is_zero(), + "fee_vault.owner must not be the zero address" + ); + eyre::ensure!( + fv.bridge_share_bps <= 10000, + "fee_vault.bridge_share_bps must be 0-10000, got {}", + fv.bridge_share_bps + ); + } + + if let (Some(ap), Some(fv)) = (&self.contracts.admin_proxy, &self.contracts.fee_vault) { + eyre::ensure!( + ap.address != fv.address, + "contracts.admin_proxy.address and contracts.fee_vault.address must be distinct" + ); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_full_config() { + let toml = r#" +[chain] +chain_id = 1234 + +[contracts.admin_proxy] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[contracts.fee_vault] +address = "0x000000000000000000000000000000000000FE00" +owner = "0x000000000000000000000000000000000000Ad00" +destination_domain = 0 +recipient_address = "0x0000000000000000000000000000000000000000000000000000000000000000" +minimum_amount = 0 +call_fee = 0 +bridge_share_bps = 10000 +other_recipient = "0x0000000000000000000000000000000000000000" +hyp_native_minter = "0x0000000000000000000000000000000000000000" +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + assert_eq!(config.chain.chain_id, 1234); + assert!(config.contracts.admin_proxy.is_some()); + assert!(config.contracts.fee_vault.is_some()); + config.validate().unwrap(); + } + + #[test] + fn reject_zero_owner() { + let toml = r#" +[chain] +chain_id = 1 + +[contracts.admin_proxy] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0x0000000000000000000000000000000000000000" +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + assert!(config.validate().is_err()); + } + + #[test] + fn reject_bps_over_10000() { + let toml = r#" +[chain] +chain_id = 1 + +[contracts.fee_vault] +address = "0x000000000000000000000000000000000000FE00" +owner = "0x000000000000000000000000000000000000Ad00" +bridge_share_bps = 10001 +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + assert!(config.validate().is_err()); + } + + #[test] + fn reject_duplicate_addresses() { + let toml = r#" +[chain] +chain_id = 1 + +[contracts.admin_proxy] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[contracts.fee_vault] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + assert!(config.validate().is_err()); + } + + #[test] + fn admin_proxy_only() { + let toml = r#" +[chain] +chain_id = 1 + +[contracts.admin_proxy] +address = "0x000000000000000000000000000000000000Ad00" +owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + config.validate().unwrap(); + assert!(config.contracts.admin_proxy.is_some()); + assert!(config.contracts.fee_vault.is_none()); + } +} diff --git a/bin/ev-deployer/src/contracts/admin_proxy.rs b/bin/ev-deployer/src/contracts/admin_proxy.rs new file mode 100644 index 00000000..ed187b12 --- /dev/null +++ b/bin/ev-deployer/src/contracts/admin_proxy.rs @@ -0,0 +1,81 @@ +//! `AdminProxy` bytecode and storage encoding. + +use crate::{config::AdminProxyConfig, contracts::GenesisContract}; +use alloy_primitives::{hex, Bytes, B256, U256}; +use std::collections::BTreeMap; + +/// `AdminProxy` runtime bytecode compiled with solc 0.8.33 (`cbor_metadata=false`). +/// Regenerate with: `cd contracts && forge inspect AdminProxy deployedBytecode` +const ADMIN_PROXY_BYTECODE: &[u8] = &hex!("60806040526004361061007e575f3560e01c80638da5cb5b1161004d5780638da5cb5b1461012d578063e30c397814610157578063f2fde38b14610181578063fa4bb79d146101a957610085565b806318dfb3c7146100895780631cff79cd146100c557806379ba5097146101015780638b5298541461011757610085565b3661008557005b5f5ffd5b348015610094575f5ffd5b506100af60048036038101906100aa9190610cf8565b6101e5565b6040516100bc9190610ea1565b60405180910390f35b3480156100d0575f5ffd5b506100eb60048036038101906100e69190610f70565b6104d9565b6040516100f89190611015565b60405180910390f35b34801561010c575f5ffd5b5061011561066c565b005b348015610122575f5ffd5b5061012b6107ed565b005b348015610138575f5ffd5b506101416108b4565b60405161014e9190611044565b60405180910390f35b348015610162575f5ffd5b5061016b6108d8565b6040516101789190611044565b60405180910390f35b34801561018c575f5ffd5b506101a760048036038101906101a2919061105d565b6108fd565b005b3480156101b4575f5ffd5b506101cf60048036038101906101ca91906110bb565b610aa4565b6040516101dc9190611015565b60405180910390f35b60605f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461026c576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8282905085859050146102ab576040517fff633a3800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8484905067ffffffffffffffff8111156102c8576102c761112c565b5b6040519080825280602002602001820160405280156102fb57816020015b60608152602001906001900390816102e65790505b5090505f5f90505b858590508110156104d0575f5f87878481811061032357610322611159565b5b9050602002016020810190610338919061105d565b73ffffffffffffffffffffffffffffffffffffffff1686868581811061036157610360611159565b5b90506020028101906103739190611192565b604051610381929190611230565b5f604051808303815f865af19150503d805f81146103ba576040519150601f19603f3d011682016040523d82523d5f602084013e6103bf565b606091505b50915091508161040657806040517fa5fa8d2b0000000000000000000000000000000000000000000000000000000081526004016103fd9190611015565b60405180910390fd5b87878481811061041957610418611159565b5b905060200201602081019061042e919061105d565b73ffffffffffffffffffffffffffffffffffffffff167fc96720f35dd524e76ea92971ce13d08e9a17816bf3b0008a7083e6032354ebb587878681811061047857610477611159565b5b905060200281019061048a9190611192565b8460405161049a93929190611274565b60405180910390a2808484815181106104b6576104b5611159565b5b602002602001018190525050508080600101915050610303565b50949350505050565b60605f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610560576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5f8573ffffffffffffffffffffffffffffffffffffffff168585604051610589929190611230565b5f604051808303815f865af19150503d805f81146105c2576040519150601f19603f3d011682016040523d82523d5f602084013e6105c7565b606091505b50915091508161060e57806040517fa5fa8d2b0000000000000000000000000000000000000000000000000000000081526004016106059190611015565b60405180910390fd5b8573ffffffffffffffffffffffffffffffffffffffff167fc96720f35dd524e76ea92971ce13d08e9a17816bf3b0008a7083e6032354ebb586868460405161065893929190611274565b60405180910390a280925050509392505050565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146106f2576040517f1853971c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff165f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3335f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505f60015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610872576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f60015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610982576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16036109e7576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8060015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508073ffffffffffffffffffffffffffffffffffffffff165f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b60605f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610b2b576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5f8673ffffffffffffffffffffffffffffffffffffffff16848787604051610b55929190611230565b5f6040518083038185875af1925050503d805f8114610b8f576040519150601f19603f3d011682016040523d82523d5f602084013e610b94565b606091505b509150915081610bdb57806040517fa5fa8d2b000000000000000000000000000000000000000000000000000000008152600401610bd29190611015565b60405180910390fd5b8673ffffffffffffffffffffffffffffffffffffffff167fc96720f35dd524e76ea92971ce13d08e9a17816bf3b0008a7083e6032354ebb5878784604051610c2593929190611274565b60405180910390a28092505050949350505050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f840112610c6357610c62610c42565b5b8235905067ffffffffffffffff811115610c8057610c7f610c46565b5b602083019150836020820283011115610c9c57610c9b610c4a565b5b9250929050565b5f5f83601f840112610cb857610cb7610c42565b5b8235905067ffffffffffffffff811115610cd557610cd4610c46565b5b602083019150836020820283011115610cf157610cf0610c4a565b5b9250929050565b5f5f5f5f60408587031215610d1057610d0f610c3a565b5b5f85013567ffffffffffffffff811115610d2d57610d2c610c3e565b5b610d3987828801610c4e565b9450945050602085013567ffffffffffffffff811115610d5c57610d5b610c3e565b5b610d6887828801610ca3565b925092505092959194509250565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f610de182610d9f565b610deb8185610da9565b9350610dfb818560208601610db9565b610e0481610dc7565b840191505092915050565b5f610e1a8383610dd7565b905092915050565b5f602082019050919050565b5f610e3882610d76565b610e428185610d80565b935083602082028501610e5485610d90565b805f5b85811015610e8f5784840389528151610e708582610e0f565b9450610e7b83610e22565b925060208a01995050600181019050610e57565b50829750879550505050505092915050565b5f6020820190508181035f830152610eb98184610e2e565b905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610eea82610ec1565b9050919050565b610efa81610ee0565b8114610f04575f5ffd5b50565b5f81359050610f1581610ef1565b92915050565b5f5f83601f840112610f3057610f2f610c42565b5b8235905067ffffffffffffffff811115610f4d57610f4c610c46565b5b602083019150836001820283011115610f6957610f68610c4a565b5b9250929050565b5f5f5f60408486031215610f8757610f86610c3a565b5b5f610f9486828701610f07565b935050602084013567ffffffffffffffff811115610fb557610fb4610c3e565b5b610fc186828701610f1b565b92509250509250925092565b5f82825260208201905092915050565b5f610fe782610d9f565b610ff18185610fcd565b9350611001818560208601610db9565b61100a81610dc7565b840191505092915050565b5f6020820190508181035f83015261102d8184610fdd565b905092915050565b61103e81610ee0565b82525050565b5f6020820190506110575f830184611035565b92915050565b5f6020828403121561107257611071610c3a565b5b5f61107f84828501610f07565b91505092915050565b5f819050919050565b61109a81611088565b81146110a4575f5ffd5b50565b5f813590506110b581611091565b92915050565b5f5f5f5f606085870312156110d3576110d2610c3a565b5b5f6110e087828801610f07565b945050602085013567ffffffffffffffff81111561110157611100610c3e565b5b61110d87828801610f1b565b93509350506040611120878288016110a7565b91505092959194509250565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f833560016020038436030381126111ae576111ad611186565b5b80840192508235915067ffffffffffffffff8211156111d0576111cf61118a565b5b6020830192506001820236038313156111ec576111eb61118e565b5b509250929050565b5f81905092915050565b828183375f83830152505050565b5f61121783856111f4565b93506112248385846111fe565b82840190509392505050565b5f61123c82848661120c565b91508190509392505050565b5f6112538385610fcd565b93506112608385846111fe565b61126983610dc7565b840190509392505050565b5f6040820190508181035f83015261128d818587611248565b905081810360208301526112a18184610fdd565b905094935050505056"); + +/// Build a genesis alloc entry for `AdminProxy`. +pub(crate) fn build(config: &AdminProxyConfig) -> GenesisContract { + let mut storage = BTreeMap::new(); + + // Slot 0: owner (address left-padded to 32 bytes) + let owner_value = B256::from(U256::from_be_bytes(config.owner.into_word().0)); + storage.insert(B256::ZERO, owner_value); + + GenesisContract { + address: config.address, + code: Bytes::from_static(ADMIN_PROXY_BYTECODE), + storage, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + use std::{path::PathBuf, process::Command}; + + #[test] + fn golden_admin_proxy_storage() { + let config = AdminProxyConfig { + address: address!("000000000000000000000000000000000000Ad00"), + owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + }; + let contract = build(&config); + + let expected_slot0: B256 = + "0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + .parse() + .unwrap(); + assert_eq!(contract.storage[&B256::ZERO], expected_slot0); + } + + #[test] + #[ignore = "requires forge CLI"] + fn admin_proxy_bytecode_matches_solidity_source() { + let contracts_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .unwrap() + .join("contracts"); + + let output = Command::new("forge") + .args(["inspect", "AdminProxy", "deployedBytecode", "--root"]) + .arg(&contracts_root) + .output() + .expect("forge not found"); + + assert!( + output.status.success(), + "forge inspect failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let forge_hex = String::from_utf8(output.stdout) + .unwrap() + .trim() + .trim_start_matches("0x") + .to_lowercase(); + + let hardcoded_hex = hex::encode(ADMIN_PROXY_BYTECODE); + + assert_eq!( + forge_hex, hardcoded_hex, + "AdminProxy bytecode mismatch! Update the constant with: cd contracts && forge inspect AdminProxy deployedBytecode" + ); + } +} diff --git a/bin/ev-deployer/src/contracts/fee_vault.rs b/bin/ev-deployer/src/contracts/fee_vault.rs new file mode 100644 index 00000000..445ea8c1 --- /dev/null +++ b/bin/ev-deployer/src/contracts/fee_vault.rs @@ -0,0 +1,184 @@ +//! `FeeVault` bytecode and storage encoding. + +use crate::{config::FeeVaultConfig, contracts::GenesisContract}; +use alloy_primitives::{hex, Bytes, B256, U256}; +use std::collections::BTreeMap; + +/// `FeeVault` runtime bytecode compiled with solc 0.8.33 (`cbor_metadata=false`). +/// Regenerate with: `cd contracts && forge inspect FeeVault deployedBytecode` +const FEE_VAULT_BYTECODE: &[u8] = &hex!("608060405260043610610101575f3560e01c80636cb53e1611610094578063bb0c829811610063578063bb0c8298146102dc578063c3f909d414610306578063eeb4a9c814610337578063f2fde38b1461035f578063f63188b71461038757610108565b80636cb53e16146102565780637d57d97a1461027e5780638da5cb5b1461028857806390321e1a146102b257610108565b806339bb1c5b116100d057806339bb1c5b146101ae5780634cebdc49146101d85780635aff5999146102025780635c4a6d841461022c57610108565b80631636b3681461010c57806326465826146101345780632858c55a1461015c5780632c2d80891461018657610108565b3661010857005b5f5ffd5b348015610117575f5ffd5b50610132600480360381019061012d919061117f565b6103af565b005b34801561013f575f5ffd5b5061015a600480360381019061015591906111dd565b610526565b005b348015610167575f5ffd5b506101706105f6565b60405161017d9190611226565b60405180910390f35b348015610191575f5ffd5b506101ac60048036038101906101a7919061129c565b61060c565b005b3480156101b9575f5ffd5b506101c2610700565b6040516101cf9190611335565b60405180910390f35b3480156101e3575f5ffd5b506101ec610724565b6040516101f9919061135d565b60405180910390f35b34801561020d575f5ffd5b50610216610749565b6040516102239190611385565b60405180910390f35b348015610237575f5ffd5b5061024061074f565b60405161024d91906113ad565b60405180910390f35b348015610261575f5ffd5b5061027c6004803603810190610277919061117f565b610755565b005b6102866108cb565b005b348015610293575f5ffd5b5061029c610caa565b6040516102a9919061135d565b60405180910390f35b3480156102bd575f5ffd5b506102c6610ccf565b6040516102d391906113ad565b60405180910390f35b3480156102e7575f5ffd5b506102f0610cd5565b6040516102fd91906113ad565b60405180910390f35b348015610311575f5ffd5b5061031a610cdb565b60405161032e9897969594939291906113c6565b60405180910390f35b348015610342575f5ffd5b5061035d600480360381019061035891906111dd565b610d81565b005b34801561036a575f5ffd5b506103856004803603810190610380919061117f565b610e51565b005b348015610392575f5ffd5b506103ad60048036038101906103a891906111dd565b61100c565b005b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461043e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610435906114c2565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16036104ac576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104a39061152a565b60405180910390fd5b8060055f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507fa50c88d04012de3892b47d81943c983dc2690cfb81f0428eaa7d382f95683e4a8160405161051b919061135d565b60405180910390a150565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146105b5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105ac906114c2565b60405180910390fd5b806004819055507f63a8f7442c91b7117b3f235d24793c034fd752a01266bef3ef1d051efb56ca3d816040516105eb91906113ad565b60405180910390a150565b600160149054906101000a900463ffffffff1681565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461069b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610692906114c2565b60405180910390fd5b81600160146101000a81548163ffffffff021916908363ffffffff160217905550806002819055507fcac2c3add78f132121267d80a684a62d521a9799fd8434bd0da1a27c491b044982826040516106f4929190611548565b60405180910390a15050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60055f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60025481565b60065481565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146107e4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107db906114c2565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1603610852576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108499061152a565b60405180910390fd5b805f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507f6eedba6e0a60268e3d78633f8822cea5dc75430d531f96fb46a29333834665c6816040516108c0919061135d565b60405180910390a150565b5f73ffffffffffffffffffffffffffffffffffffffff165f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1603610959576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610950906115b9565b60405180910390fd5b60045434101561099e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161099590611621565b60405180910390fd5b5f4790505f612710600654836109b4919061166c565b6109be91906116da565b90505f81836109cd919061170a565b9050600354821015610a14576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a0b90611787565b60405180910390fd5b7f50ecfcc47f2c5b2a26f91422abf650476ec7f701c48b1cf6d1d6d4d51a872ed6838383604051610a47939291906117a5565b60405180910390a15f811115610bb1575f73ffffffffffffffffffffffffffffffffffffffff1660055f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1603610ae6576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610add9061184a565b60405180910390fd5b5f60055f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1682604051610b2c90611895565b5f6040518083038185875af1925050503d805f8114610b66576040519150601f19603f3d011682016040523d82523d5f602084013e610b6b565b606091505b5050905080610baf576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610ba6906118f3565b60405180910390fd5b505b5f5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381b4e8b484600160149054906101000a900463ffffffff16600254876040518563ffffffff1660e01b8152600401610c2493929190611911565b60206040518083038185885af1158015610c40573d5f5f3e3d5ffd5b50505050506040513d601f19601f82011682018060405250810190610c65919061195a565b90507f301fb78c068680a9fb5daa4ebadf5914ddc3a317f1fdc2c97f32740374d61e748360025483604051610c9c93929190611985565b60405180910390a150505050565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60045481565b60035481565b5f5f5f5f5f5f5f5f60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff16600160149054906101000a900463ffffffff1660025460035460045460065460055f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff165f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff16975097509750975097509750975097509091929394959697565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610e10576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610e07906114c2565b60405180910390fd5b806003819055507f6ea576632a91ef2f8d4ee43600561b386f3c0254692977f0d33e17742bc5355881604051610e4691906113ad565b60405180910390a150565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610ee0576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610ed7906114c2565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1603610f4e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f4590611a2a565b60405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff1660015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a38060015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461109b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611092906114c2565b60405180910390fd5b6127108111156110e0576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110d790611a92565b60405180910390fd5b806006819055507fa8da92ecf88f6d9f058e5f86d614520d5f20a3ecf87914deb605f649bd63de878160405161111691906113ad565b60405180910390a150565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61114e82611125565b9050919050565b61115e81611144565b8114611168575f5ffd5b50565b5f8135905061117981611155565b92915050565b5f6020828403121561119457611193611121565b5b5f6111a18482850161116b565b91505092915050565b5f819050919050565b6111bc816111aa565b81146111c6575f5ffd5b50565b5f813590506111d7816111b3565b92915050565b5f602082840312156111f2576111f1611121565b5b5f6111ff848285016111c9565b91505092915050565b5f63ffffffff82169050919050565b61122081611208565b82525050565b5f6020820190506112395f830184611217565b92915050565b61124881611208565b8114611252575f5ffd5b50565b5f813590506112638161123f565b92915050565b5f819050919050565b61127b81611269565b8114611285575f5ffd5b50565b5f8135905061129681611272565b92915050565b5f5f604083850312156112b2576112b1611121565b5b5f6112bf85828601611255565b92505060206112d085828601611288565b9150509250929050565b5f819050919050565b5f6112fd6112f86112f384611125565b6112da565b611125565b9050919050565b5f61130e826112e3565b9050919050565b5f61131f82611304565b9050919050565b61132f81611315565b82525050565b5f6020820190506113485f830184611326565b92915050565b61135781611144565b82525050565b5f6020820190506113705f83018461134e565b92915050565b61137f81611269565b82525050565b5f6020820190506113985f830184611376565b92915050565b6113a7816111aa565b82525050565b5f6020820190506113c05f83018461139e565b92915050565b5f610100820190506113da5f83018b61134e565b6113e7602083018a611217565b6113f46040830189611376565b611401606083018861139e565b61140e608083018761139e565b61141b60a083018661139e565b61142860c083018561134e565b61143560e083018461134e565b9998505050505050505050565b5f82825260208201905092915050565b7f4665655661756c743a2063616c6c6572206973206e6f7420746865206f776e655f8201527f7200000000000000000000000000000000000000000000000000000000000000602082015250565b5f6114ac602183611442565b91506114b782611452565b604082019050919050565b5f6020820190508181035f8301526114d9816114a0565b9050919050565b7f4665655661756c743a207a65726f2061646472657373000000000000000000005f82015250565b5f611514601683611442565b915061151f826114e0565b602082019050919050565b5f6020820190508181035f83015261154181611508565b9050919050565b5f60408201905061155b5f830185611217565b6115686020830184611376565b9392505050565b7f4665655661756c743a206d696e746572206e6f742073657400000000000000005f82015250565b5f6115a3601883611442565b91506115ae8261156f565b602082019050919050565b5f6020820190508181035f8301526115d081611597565b9050919050565b7f4665655661756c743a20696e73756666696369656e74206665650000000000005f82015250565b5f61160b601a83611442565b9150611616826115d7565b602082019050919050565b5f6020820190508181035f830152611638816115ff565b9050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f611676826111aa565b9150611681836111aa565b925082820261168f816111aa565b915082820484148315176116a6576116a561163f565b5b5092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f6116e4826111aa565b91506116ef836111aa565b9250826116ff576116fe6116ad565b5b828204905092915050565b5f611714826111aa565b915061171f836111aa565b92508282039050818111156117375761173661163f565b5b92915050565b7f4665655661756c743a206d696e696d756d20616d6f756e74206e6f74206d65745f82015250565b5f611771602083611442565b915061177c8261173d565b602082019050919050565b5f6020820190508181035f83015261179e81611765565b9050919050565b5f6060820190506117b85f83018661139e565b6117c5602083018561139e565b6117d2604083018461139e565b949350505050565b7f4665655661756c743a206f7468657220726563697069656e74206e6f742073655f8201527f7400000000000000000000000000000000000000000000000000000000000000602082015250565b5f611834602183611442565b915061183f826117da565b604082019050919050565b5f6020820190508181035f83015261186181611828565b9050919050565b5f81905092915050565b50565b5f6118805f83611868565b915061188b82611872565b5f82019050919050565b5f61189f82611875565b9150819050919050565b7f4665655661756c743a207472616e73666572206661696c6564000000000000005f82015250565b5f6118dd601983611442565b91506118e8826118a9565b602082019050919050565b5f6020820190508181035f83015261190a816118d1565b9050919050565b5f6060820190506119245f830186611217565b6119316020830185611376565b61193e604083018461139e565b949350505050565b5f8151905061195481611272565b92915050565b5f6020828403121561196f5761196e611121565b5b5f61197c84828501611946565b91505092915050565b5f6060820190506119985f83018661139e565b6119a56020830185611376565b6119b26040830184611376565b949350505050565b7f4665655661756c743a206e6577206f776e657220697320746865207a65726f205f8201527f6164647265737300000000000000000000000000000000000000000000000000602082015250565b5f611a14602783611442565b9150611a1f826119ba565b604082019050919050565b5f6020820190508181035f830152611a4181611a08565b9050919050565b7f4665655661756c743a20696e76616c69642062707300000000000000000000005f82015250565b5f611a7c601583611442565b9150611a8782611a48565b602082019050919050565b5f6020820190508181035f830152611aa981611a70565b905091905056"); + +/// Build a genesis alloc entry for `FeeVault`. +pub(crate) fn build(config: &FeeVaultConfig) -> GenesisContract { + let mut storage = BTreeMap::new(); + + // Apply constructor default: bps 0 -> 10000 + let effective_bps = if config.bridge_share_bps == 0 { + 10000 + } else { + config.bridge_share_bps + }; + + // Slot 0: hypNativeMinter (address) + storage.insert( + B256::ZERO, + B256::from(U256::from_be_bytes(config.hyp_native_minter.into_word().0)), + ); + + // Slot 1: owner (lower 160 bits) + destinationDomain (shifted left 160 bits) + let owner_u256 = U256::from_be_bytes(config.owner.into_word().0); + let domain_u256 = U256::from(config.destination_domain) << 160; + storage.insert( + B256::with_last_byte(1), + B256::from(owner_u256 | domain_u256), + ); + + // Slot 2: recipientAddress (bytes32) + storage.insert(B256::with_last_byte(2), config.recipient_address); + + // Slot 3: minimumAmount + storage.insert( + B256::with_last_byte(3), + B256::from(U256::from(config.minimum_amount)), + ); + + // Slot 4: callFee + storage.insert( + B256::with_last_byte(4), + B256::from(U256::from(config.call_fee)), + ); + + // Slot 5: otherRecipient (address) + storage.insert( + B256::with_last_byte(5), + B256::from(U256::from_be_bytes(config.other_recipient.into_word().0)), + ); + + // Slot 6: bridgeShareBps + storage.insert( + B256::with_last_byte(6), + B256::from(U256::from(effective_bps)), + ); + + GenesisContract { + address: config.address, + code: Bytes::from_static(FEE_VAULT_BYTECODE), + storage, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, Address}; + use std::{path::PathBuf, process::Command}; + + #[test] + fn fee_vault_storage_encoding() { + let config = FeeVaultConfig { + address: address!("000000000000000000000000000000000000FE00"), + owner: address!("000000000000000000000000000000000000Ad00"), + destination_domain: 0, + recipient_address: B256::ZERO, + minimum_amount: 0, + call_fee: 0, + bridge_share_bps: 10000, + other_recipient: Address::ZERO, + hyp_native_minter: Address::ZERO, + }; + let contract = build(&config); + + // Slot 0: hypNativeMinter = zero + assert_eq!(contract.storage[&B256::ZERO], B256::ZERO); + + // Slot 1: owner packed with domain + let expected_slot1: B256 = + "0x000000000000000000000000000000000000000000000000000000000000Ad00" + .parse() + .unwrap(); + assert_eq!(contract.storage[&B256::with_last_byte(1)], expected_slot1); + + // Slot 6: bridgeShareBps = 10000 + let expected_slot6 = B256::from(U256::from(10000u64)); + assert_eq!(contract.storage[&B256::with_last_byte(6)], expected_slot6); + } + + #[test] + fn bps_zero_defaults_to_10000() { + let config = FeeVaultConfig { + address: address!("000000000000000000000000000000000000FE00"), + owner: address!("000000000000000000000000000000000000Ad00"), + destination_domain: 0, + recipient_address: B256::ZERO, + minimum_amount: 0, + call_fee: 0, + bridge_share_bps: 0, + other_recipient: Address::ZERO, + hyp_native_minter: Address::ZERO, + }; + let contract = build(&config); + + let expected_slot6 = B256::from(U256::from(10000u64)); + assert_eq!(contract.storage[&B256::with_last_byte(6)], expected_slot6); + } + + #[test] + fn slot1_packing_with_nonzero_domain() { + let config = FeeVaultConfig { + address: address!("000000000000000000000000000000000000FE00"), + owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + destination_domain: 42, + recipient_address: B256::ZERO, + minimum_amount: 0, + call_fee: 0, + bridge_share_bps: 10000, + other_recipient: Address::ZERO, + hyp_native_minter: Address::ZERO, + }; + let contract = build(&config); + + // slot1 = (42 << 160) | owner + let owner_u256 = U256::from_be_bytes( + address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266") + .into_word() + .0, + ); + let expected = B256::from((U256::from(42u32) << 160) | owner_u256); + assert_eq!(contract.storage[&B256::with_last_byte(1)], expected); + } + + #[test] + #[ignore = "requires forge CLI"] + fn fee_vault_bytecode_matches_solidity_source() { + let contracts_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .unwrap() + .join("contracts"); + + let output = Command::new("forge") + .args(["inspect", "FeeVault", "deployedBytecode", "--root"]) + .arg(&contracts_root) + .output() + .expect("forge not found"); + + assert!( + output.status.success(), + "forge inspect failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let forge_hex = String::from_utf8(output.stdout) + .unwrap() + .trim() + .trim_start_matches("0x") + .to_lowercase(); + + let hardcoded_hex = hex::encode(FEE_VAULT_BYTECODE); + + assert_eq!( + forge_hex, hardcoded_hex, + "FeeVault bytecode mismatch! Update the constant with: cd contracts && forge inspect FeeVault deployedBytecode" + ); + } +} diff --git a/bin/ev-deployer/src/contracts/mod.rs b/bin/ev-deployer/src/contracts/mod.rs new file mode 100644 index 00000000..8ef01558 --- /dev/null +++ b/bin/ev-deployer/src/contracts/mod.rs @@ -0,0 +1,17 @@ +//! Contract bytecode and storage encoding. + +pub(crate) mod admin_proxy; +pub(crate) mod fee_vault; + +use alloy_primitives::{Address, Bytes, B256}; +use std::collections::BTreeMap; + +/// A contract ready to be placed in genesis alloc. +pub(crate) struct GenesisContract { + /// The address to deploy at. + pub address: Address, + /// Runtime bytecode. + pub code: Bytes, + /// Storage slot values. + pub storage: BTreeMap, +} diff --git a/bin/ev-deployer/src/genesis.rs b/bin/ev-deployer/src/genesis.rs new file mode 100644 index 00000000..9b200769 --- /dev/null +++ b/bin/ev-deployer/src/genesis.rs @@ -0,0 +1,179 @@ +//! Genesis alloc JSON builder. + +use crate::{ + config::DeployConfig, + contracts::{self, GenesisContract}, +}; +use alloy_primitives::B256; +use serde_json::{Map, Value}; +use std::path::Path; + +/// Build the alloc JSON from config. +pub(crate) fn build_alloc(config: &DeployConfig) -> Value { + let mut alloc = Map::new(); + + if let Some(ref ap_config) = config.contracts.admin_proxy { + let contract = contracts::admin_proxy::build(ap_config); + insert_contract(&mut alloc, &contract); + } + + if let Some(ref fv_config) = config.contracts.fee_vault { + let contract = contracts::fee_vault::build(fv_config); + insert_contract(&mut alloc, &contract); + } + + Value::Object(alloc) +} + +/// Build alloc and merge into an existing genesis JSON file. +pub(crate) fn merge_into( + config: &DeployConfig, + genesis_path: &Path, + force: bool, +) -> eyre::Result { + let content = std::fs::read_to_string(genesis_path)?; + let mut genesis: Value = serde_json::from_str(&content)?; + + let alloc = build_alloc(config); + + let genesis_alloc = genesis + .get_mut("alloc") + .and_then(|v| v.as_object_mut()) + .ok_or_else(|| eyre::eyre!("genesis JSON missing 'alloc' object"))?; + + let new_alloc = alloc.as_object().unwrap(); + for (addr, entry) in new_alloc { + if genesis_alloc.contains_key(addr) && !force { + eyre::bail!("address collision at {addr}; use --force to overwrite"); + } + genesis_alloc.insert(addr.clone(), entry.clone()); + } + + Ok(genesis) +} + +fn insert_contract(alloc: &mut Map, contract: &GenesisContract) { + // Address key without 0x prefix, using checksummed format + let addr_hex = format!("{}", contract.address); + let addr_key = addr_hex.strip_prefix("0x").unwrap_or(&addr_hex); + + let mut storage_map = Map::new(); + for (slot, value) in &contract.storage { + let slot_key = format_slot_key(slot); + storage_map.insert(slot_key, Value::String(format!("{value}"))); + } + + let mut entry = Map::new(); + entry.insert("balance".to_string(), Value::String("0x0".to_string())); + entry.insert( + "code".to_string(), + Value::String(format!( + "0x{}", + alloy_primitives::hex::encode(&contract.code) + )), + ); + entry.insert("storage".to_string(), Value::Object(storage_map)); + + alloc.insert(addr_key.to_string(), Value::Object(entry)); +} + +/// Format a storage slot key as a full 32-byte hex string. +/// `B256::ZERO` -> "0x0000000000000000000000000000000000000000000000000000000000000000" +fn format_slot_key(slot: &B256) -> String { + format!("{slot}") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::*; + use alloy_primitives::address; + + fn test_config() -> DeployConfig { + DeployConfig { + chain: ChainConfig { chain_id: 1234 }, + contracts: ContractsConfig { + admin_proxy: Some(AdminProxyConfig { + address: address!("000000000000000000000000000000000000Ad00"), + owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + }), + fee_vault: None, + }, + } + } + + #[test] + fn alloc_json_structure() { + let alloc = build_alloc(&test_config()); + let obj = alloc.as_object().unwrap(); + assert!(obj.contains_key("000000000000000000000000000000000000Ad00")); + + let entry = obj + .get("000000000000000000000000000000000000Ad00") + .unwrap() + .as_object() + .unwrap(); + assert_eq!(entry["balance"], "0x0"); + assert!(entry["code"].as_str().unwrap().starts_with("0x")); + assert!(entry.contains_key("storage")); + } + + #[test] + fn alloc_golden_value() { + let alloc = build_alloc(&test_config()); + let storage = alloc + .as_object() + .unwrap() + .get("000000000000000000000000000000000000Ad00") + .unwrap() + .get("storage") + .unwrap() + .as_object() + .unwrap(); + + assert_eq!( + storage["0x0000000000000000000000000000000000000000000000000000000000000000"], + "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ); + } + + #[test] + fn slot_key_formatting() { + assert_eq!( + format_slot_key(&B256::ZERO), + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!( + format_slot_key(&B256::with_last_byte(1)), + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert_eq!( + format_slot_key(&B256::with_last_byte(6)), + "0x0000000000000000000000000000000000000000000000000000000000000006" + ); + } + + #[test] + fn merge_detects_collision() { + let genesis = r#"{"alloc":{"000000000000000000000000000000000000Ad00":{"balance":"0x0"}}}"#; + let tmp = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(tmp.path(), genesis).unwrap(); + + let result = merge_into(&test_config(), tmp.path(), false); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("address collision")); + } + + #[test] + fn merge_force_overwrites() { + let genesis = r#"{"alloc":{"000000000000000000000000000000000000Ad00":{"balance":"0x0"}}}"#; + let tmp = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(tmp.path(), genesis).unwrap(); + + let result = merge_into(&test_config(), tmp.path(), true); + assert!(result.is_ok()); + } +} diff --git a/bin/ev-deployer/src/main.rs b/bin/ev-deployer/src/main.rs new file mode 100644 index 00000000..42ad6a4a --- /dev/null +++ b/bin/ev-deployer/src/main.rs @@ -0,0 +1,120 @@ +//! EV Deployer — genesis alloc generator for ev-reth contracts. + +mod config; +mod contracts; +mod genesis; +mod output; + +use clap::{Parser, Subcommand}; +use std::path::PathBuf; + +/// EV Deployer: generate genesis alloc entries for ev-reth contracts. +#[derive(Parser)] +#[command( + name = "ev-deployer", + about = "Generate genesis alloc for ev-reth contracts" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Generate genesis alloc JSON from a deploy config. + Genesis { + /// Path to the deploy TOML config. + #[arg(long)] + config: PathBuf, + + /// Write alloc JSON to this file instead of stdout. + #[arg(long)] + output: Option, + + /// Merge alloc entries into an existing genesis JSON file. + #[arg(long)] + merge_into: Option, + + /// Allow overwriting existing addresses when merging. + #[arg(long, default_value_t = false)] + force: bool, + + /// Write an address manifest to this file. + #[arg(long)] + addresses_out: Option, + }, + /// Compute the address for a configured contract. + ComputeAddress { + /// Path to the deploy TOML config. + #[arg(long)] + config: PathBuf, + + /// Contract name (`admin_proxy` or `fee_vault`). + #[arg(long)] + contract: String, + }, +} + +fn main() -> eyre::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Command::Genesis { + config: config_path, + output, + merge_into, + force, + addresses_out, + } => { + let cfg = config::DeployConfig::load(&config_path)?; + + let result = if let Some(ref genesis_path) = merge_into { + genesis::merge_into(&cfg, genesis_path, force)? + } else { + genesis::build_alloc(&cfg) + }; + + let json = serde_json::to_string_pretty(&result)?; + + if let Some(ref out_path) = output { + std::fs::write(out_path, &json)?; + eprintln!("Wrote alloc to {}", out_path.display()); + } else { + println!("{json}"); + } + + if let Some(ref addr_path) = addresses_out { + let manifest = output::build_manifest(&cfg); + let manifest_json = serde_json::to_string_pretty(&manifest)?; + std::fs::write(addr_path, &manifest_json)?; + eprintln!("Wrote address manifest to {}", addr_path.display()); + } + } + Command::ComputeAddress { + config: config_path, + contract, + } => { + let cfg = config::DeployConfig::load(&config_path)?; + + let address = match contract.as_str() { + "admin_proxy" => cfg + .contracts + .admin_proxy + .as_ref() + .map(|c| c.address) + .ok_or_else(|| eyre::eyre!("admin_proxy not configured"))?, + "fee_vault" => cfg + .contracts + .fee_vault + .as_ref() + .map(|c| c.address) + .ok_or_else(|| eyre::eyre!("fee_vault not configured"))?, + other => eyre::bail!("unknown contract: {other}"), + }; + + println!("{address}"); + } + } + + Ok(()) +} diff --git a/bin/ev-deployer/src/output.rs b/bin/ev-deployer/src/output.rs new file mode 100644 index 00000000..22bf063c --- /dev/null +++ b/bin/ev-deployer/src/output.rs @@ -0,0 +1,25 @@ +//! Address manifest output. + +use crate::config::DeployConfig; +use serde_json::{Map, Value}; + +/// Build an address manifest JSON from config. +pub(crate) fn build_manifest(config: &DeployConfig) -> Value { + let mut manifest = Map::new(); + + if let Some(ref ap) = config.contracts.admin_proxy { + manifest.insert( + "admin_proxy".to_string(), + Value::String(format!("{}", ap.address)), + ); + } + + if let Some(ref fv) = config.contracts.fee_vault { + manifest.insert( + "fee_vault".to_string(), + Value::String(format!("{}", fv.address)), + ); + } + + Value::Object(manifest) +} diff --git a/bin/ev-deployer/tests/e2e_genesis.sh b/bin/ev-deployer/tests/e2e_genesis.sh new file mode 100755 index 00000000..3b7783b4 --- /dev/null +++ b/bin/ev-deployer/tests/e2e_genesis.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# End-to-end test: generate genesis with ev-deployer, boot ev-reth, verify contracts via RPC. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DEPLOYER="$REPO_ROOT/target/release/ev-deployer" +EV_RETH="$REPO_ROOT/target/release/ev-reth" +CONFIG="$REPO_ROOT/bin/ev-deployer/examples/devnet.toml" +BASE_GENESIS="$REPO_ROOT/bin/ev-dev/assets/devnet-genesis.json" + +RPC_PORT=18545 +RPC_URL="http://127.0.0.1:$RPC_PORT" +NODE_PID="" +TMPDIR_PATH="" + +cleanup() { + if [[ -n "$NODE_PID" ]]; then + kill "$NODE_PID" 2>/dev/null || true + wait "$NODE_PID" 2>/dev/null || true + fi + if [[ -n "$TMPDIR_PATH" ]]; then + rm -rf "$TMPDIR_PATH" + fi +} +trap cleanup EXIT + +# ── Helpers ────────────────────────────────────────────── + +fail() { echo "FAIL: $1" >&2; exit 1; } +pass() { echo "PASS: $1"; } + +rpc_call() { + local method="$1" + local params="$2" + curl -s --connect-timeout 5 --max-time 10 -X POST "$RPC_URL" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['result'])" +} + +wait_for_rpc() { + local max_attempts=30 + for i in $(seq 1 $max_attempts); do + if curl -s --connect-timeout 1 --max-time 2 -X POST "$RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + 2>/dev/null | grep -q result; then + return 0 + fi + sleep 1 + done + fail "node did not become ready after ${max_attempts}s" +} + +# ── Step 1: Build ──────────────────────────────────────── + +echo "=== Building ev-deployer and ev-reth ===" +cargo build --release --bin ev-deployer --bin ev-reth --manifest-path "$REPO_ROOT/Cargo.toml" \ + 2>&1 | tail -3 + +[[ -x "$DEPLOYER" ]] || fail "ev-deployer binary not found" +[[ -x "$EV_RETH" ]] || fail "ev-reth binary not found" + +# ── Step 2: Generate genesis ───────────────────────────── + +TMPDIR_PATH="$(mktemp -d)" +GENESIS="$TMPDIR_PATH/genesis.json" +DATADIR="$TMPDIR_PATH/data" + +echo "=== Generating genesis with ev-deployer ===" +"$DEPLOYER" genesis \ + --config "$CONFIG" \ + --merge-into "$BASE_GENESIS" \ + --output "$GENESIS" \ + --force + +echo "Genesis written to $GENESIS" + +# Quick sanity: addresses should be in the alloc +grep -q "000000000000000000000000000000000000Ad00" "$GENESIS" \ + || fail "AdminProxy address not found in genesis" +grep -q "000000000000000000000000000000000000FE00" "$GENESIS" \ + || fail "FeeVault address not found in genesis" + +pass "genesis contains both contract addresses" + +# ── Step 3: Start ev-reth ──────────────────────────────── + +echo "=== Starting ev-reth node ===" +"$EV_RETH" node \ + --dev \ + --chain "$GENESIS" \ + --datadir "$DATADIR" \ + --http \ + --http.addr 127.0.0.1 \ + --http.port "$RPC_PORT" \ + --http.api eth,net,web3 \ + --disable-discovery \ + --no-persist-peers \ + --port 0 \ + --log.stdout.filter error \ + & +NODE_PID=$! + +echo "Node PID: $NODE_PID, waiting for RPC..." +wait_for_rpc +pass "node is up and responding to RPC" + +# ── Step 4: Verify AdminProxy ──────────────────────────── + +ADMIN_PROXY="0x000000000000000000000000000000000000Ad00" +ADMIN_OWNER="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +echo "=== Verifying AdminProxy at $ADMIN_PROXY ===" + +# Check code is present +admin_code=$(rpc_call "eth_getCode" "[\"$ADMIN_PROXY\", \"latest\"]") +[[ "$admin_code" != "0x" && "$admin_code" != "0x0" && ${#admin_code} -gt 10 ]] \ + || fail "AdminProxy has no bytecode (got: $admin_code)" +pass "AdminProxy has bytecode (${#admin_code} hex chars)" + +# Check owner in slot 0 +admin_slot0=$(rpc_call "eth_getStorageAt" "[\"$ADMIN_PROXY\", \"0x0\", \"latest\"]") +# Owner should be in the lower 20 bytes, left-padded to 32 bytes +expected_owner_slot="0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" +[[ "$(echo "$admin_slot0" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_owner_slot" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "AdminProxy slot 0 (owner) mismatch: got $admin_slot0, expected $expected_owner_slot" +pass "AdminProxy owner slot 0 = $ADMIN_OWNER" + +# ── Step 5: Verify FeeVault ────────────────────────────── + +FEE_VAULT="0x000000000000000000000000000000000000FE00" +FEE_VAULT_OWNER="0x000000000000000000000000000000000000Ad00" + +echo "=== Verifying FeeVault at $FEE_VAULT ===" + +# Check code is present +fv_code=$(rpc_call "eth_getCode" "[\"$FEE_VAULT\", \"latest\"]") +[[ "$fv_code" != "0x" && "$fv_code" != "0x0" && ${#fv_code} -gt 10 ]] \ + || fail "FeeVault has no bytecode (got: $fv_code)" +pass "FeeVault has bytecode (${#fv_code} hex chars)" + +# Slot 0: hypNativeMinter (should be zero) +fv_slot0=$(rpc_call "eth_getStorageAt" "[\"$FEE_VAULT\", \"0x0\", \"latest\"]") +expected_zero="0x0000000000000000000000000000000000000000000000000000000000000000" +[[ "$(echo "$fv_slot0" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_zero" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "FeeVault slot 0 (hypNativeMinter) should be zero, got $fv_slot0" +pass "FeeVault slot 0 (hypNativeMinter) = zero" + +# Slot 1: owner (lower 160 bits) + destinationDomain (upper bits) +# With domain=0 and owner=0x...Ad00, it's just the owner padded +fv_slot1=$(rpc_call "eth_getStorageAt" "[\"$FEE_VAULT\", \"0x1\", \"latest\"]") +expected_slot1="0x000000000000000000000000000000000000000000000000000000000000ad00" +[[ "$(echo "$fv_slot1" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_slot1" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "FeeVault slot 1 (owner|domain) mismatch: got $fv_slot1, expected $expected_slot1" +pass "FeeVault slot 1 (owner|domain) correct" + +# Slot 6: bridgeShareBps = 10000 = 0x2710 +fv_slot6=$(rpc_call "eth_getStorageAt" "[\"$FEE_VAULT\", \"0x6\", \"latest\"]") +expected_slot6="0x0000000000000000000000000000000000000000000000000000000000002710" +[[ "$(echo "$fv_slot6" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_slot6" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "FeeVault slot 6 (bridgeShareBps) mismatch: got $fv_slot6, expected $expected_slot6" +pass "FeeVault slot 6 (bridgeShareBps) = 10000" + +# ── Done ───────────────────────────────────────────────── + +echo "" +echo "=== All checks passed ===" diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 00000000..aee2c9a8 --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,5 @@ +{ + "lib/forge-std": { + "rev": "887e87251562513a7b5ab1ea517c039fe6ee0984" + } +} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 25b918f9..7bfd1700 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -2,5 +2,8 @@ src = "src" out = "out" libs = ["lib"] +solc_version = "0.8.33" +cbor_metadata = false +bytecode_hash = "none" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/justfile b/justfile index 935c3a03..757a5b96 100644 --- a/justfile +++ b/justfile @@ -34,6 +34,10 @@ build-maxperf: build-all: {{cargo}} build --workspace --release +# Build the ev-deployer binary in release mode +build-deployer: + {{cargo}} build --release --bin ev-deployer + # Testing ────────────────────────────────────────────── # Run all tests @@ -64,6 +68,10 @@ test-evolve: test-common: {{cargo}} test -p ev-common +# Test the deployer crate +test-deployer: + {{cargo}} test -p ev-deployer + # Development ────────────────────────────────────────── # Run the ev-reth node with default settings