diff --git a/.github/workflows/ev_deployer.yml b/.github/workflows/ev_deployer.yml index ef57c12a..ef80e7e0 100644 --- a/.github/workflows/ev_deployer.yml +++ b/.github/workflows/ev_deployer.yml @@ -40,6 +40,9 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + - name: Install Hyperlane soldeer dependencies + run: cd contracts/lib/hyperlane-monorepo/solidity && forge soldeer install + - name: Run bytecode verification tests run: cargo test -p ev-deployer -- --ignored --test-threads=1 diff --git a/.gitmodules b/.gitmodules index c65a5965..735b8dc8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/forge-std"] path = contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/hyperlane-monorepo"] + path = contracts/lib/hyperlane-monorepo + url = https://github.com/hyperlane-xyz/hyperlane-monorepo.git diff --git a/bin/ev-deployer/examples/devnet.toml b/bin/ev-deployer/examples/devnet.toml index f332b1ad..91f033ca 100644 --- a/bin/ev-deployer/examples/devnet.toml +++ b/bin/ev-deployer/examples/devnet.toml @@ -15,3 +15,25 @@ call_fee = 0 bridge_share_bps = 10000 other_recipient = "0x0000000000000000000000000000000000000000" hyp_native_minter = "0x0000000000000000000000000000000000000000" + +[contracts.mailbox] +address = "0x0000000000000000000000000000000000001200" +owner = "0x000000000000000000000000000000000000Ad00" +default_ism = "0x0000000000000000000000000000000000001300" +default_hook = "0x0000000000000000000000000000000000001400" +required_hook = "0x0000000000000000000000000000000000001100" + +[contracts.merkle_tree_hook] +address = "0x0000000000000000000000000000000000001100" +owner = "0x000000000000000000000000000000000000Ad00" +mailbox = "0x0000000000000000000000000000000000001200" + +[contracts.noop_ism] +address = "0x0000000000000000000000000000000000001300" + +[contracts.protocol_fee] +address = "0x0000000000000000000000000000000000001400" +owner = "0x000000000000000000000000000000000000Ad00" +max_protocol_fee = 1000000000000000000 +protocol_fee = 0 +beneficiary = "0x000000000000000000000000000000000000Ad00" diff --git a/bin/ev-deployer/src/config.rs b/bin/ev-deployer/src/config.rs index 66fb1c24..1b7540eb 100644 --- a/bin/ev-deployer/src/config.rs +++ b/bin/ev-deployer/src/config.rs @@ -29,6 +29,14 @@ pub(crate) struct ContractsConfig { pub admin_proxy: Option, /// `FeeVault` contract config (optional). pub fee_vault: Option, + /// `MerkleTreeHook` contract config (optional). + pub merkle_tree_hook: Option, + /// `Mailbox` contract config (optional). + pub mailbox: Option, + /// `NoopIsm` contract config (optional). + pub noop_ism: Option, + /// `ProtocolFee` contract config (optional). + pub protocol_fee: Option, } /// `AdminProxy` configuration. @@ -70,6 +78,62 @@ pub(crate) struct FeeVaultConfig { pub hyp_native_minter: Address, } +/// `MerkleTreeHook` configuration (Hyperlane required hook). +#[derive(Debug, Deserialize)] +pub(crate) struct MerkleTreeHookConfig { + /// Address to deploy at. + pub address: Address, + /// Owner address (for post-genesis hook/ISM changes). + #[serde(default)] + pub owner: Address, + /// Mailbox address (patched into bytecode as immutable). + pub mailbox: Address, +} + +/// `ProtocolFee` configuration (Hyperlane post-dispatch hook that charges a protocol fee). +#[derive(Debug, Deserialize)] +pub(crate) struct ProtocolFeeConfig { + /// Address to deploy at. + pub address: Address, + /// Owner address. + #[serde(default)] + pub owner: Address, + /// Maximum protocol fee in wei. + pub max_protocol_fee: u64, + /// Protocol fee charged per dispatch in wei. + #[serde(default)] + pub protocol_fee: u64, + /// Beneficiary address that receives collected fees. + #[serde(default)] + pub beneficiary: Address, +} + +/// `Mailbox` configuration (Hyperlane core messaging hub). +#[derive(Debug, Deserialize)] +pub(crate) struct MailboxConfig { + /// Address to deploy at. + pub address: Address, + /// Owner address. + #[serde(default)] + pub owner: Address, + /// Default interchain security module. + #[serde(default)] + pub default_ism: Address, + /// Default post-dispatch hook. + #[serde(default)] + pub default_hook: Address, + /// Required post-dispatch hook (e.g. `MerkleTreeHook`). + #[serde(default)] + pub required_hook: Address, +} + +/// `NoopIsm` configuration (Hyperlane ISM that accepts all messages). +#[derive(Debug, Deserialize)] +pub(crate) struct NoopIsmConfig { + /// Address to deploy at. + pub address: Address, +} + impl DeployConfig { /// Load and validate config from a TOML file. pub(crate) fn load(path: &Path) -> eyre::Result { @@ -100,6 +164,24 @@ impl DeployConfig { ); } + if let Some(ref mth) = self.contracts.merkle_tree_hook { + eyre::ensure!( + !mth.mailbox.is_zero(), + "merkle_tree_hook.mailbox must not be the zero address" + ); + } + + if let Some(ref pf) = self.contracts.protocol_fee { + eyre::ensure!( + !pf.owner.is_zero(), + "protocol_fee.owner must not be the zero address" + ); + eyre::ensure!( + !pf.beneficiary.is_zero(), + "protocol_fee.beneficiary must not be the zero address" + ); + } + if let (Some(ap), Some(fv)) = (&self.contracts.admin_proxy, &self.contracts.fee_vault) { eyre::ensure!( ap.address != fv.address, @@ -172,6 +254,38 @@ bridge_share_bps = 10001 assert!(config.validate().is_err()); } + #[test] + fn parse_merkle_tree_hook_config() { + let toml = r#" +[chain] +chain_id = 1234 + +[contracts.merkle_tree_hook] +address = "0x0000000000000000000000000000000000001100" +owner = "0x000000000000000000000000000000000000ad00" +mailbox = "0x0000000000000000000000000000000000001200" +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + config.validate().unwrap(); + assert!(config.contracts.merkle_tree_hook.is_some()); + let mth = config.contracts.merkle_tree_hook.unwrap(); + assert!(!mth.mailbox.is_zero()); + } + + #[test] + fn reject_zero_mailbox_merkle_tree_hook() { + let toml = r#" +[chain] +chain_id = 1 + +[contracts.merkle_tree_hook] +address = "0x0000000000000000000000000000000000001100" +mailbox = "0x0000000000000000000000000000000000000000" +"#; + let config: DeployConfig = toml::from_str(toml).unwrap(); + assert!(config.validate().is_err()); + } + #[test] fn reject_duplicate_addresses() { let toml = r#" diff --git a/bin/ev-deployer/src/contracts/immutables.rs b/bin/ev-deployer/src/contracts/immutables.rs new file mode 100644 index 00000000..40c75f48 --- /dev/null +++ b/bin/ev-deployer/src/contracts/immutables.rs @@ -0,0 +1,127 @@ +//! Bytecode patching for Solidity immutable variables. +//! +//! Solidity `immutable` values are embedded in the **runtime bytecode** by the +//! compiler, not in storage. When compiling with placeholder values (e.g. +//! `address(0)`, `uint32(0)`), the compiler leaves zero-filled regions at known +//! byte offsets. This module replaces those regions with the actual values from +//! the deploy config at genesis-generation time. + +use alloy_primitives::{Address, B256, U256}; + +/// A single immutable reference inside a bytecode blob. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ImmutableRef { + /// Byte offset into the **runtime** bytecode. + pub start: usize, + /// Number of bytes (always 32 for EVM words). + pub length: usize, +} + +/// Patch a mutable bytecode slice, writing `value` at every listed offset. +/// +/// # Panics +/// +/// Panics if any reference extends past the end of `bytecode`. +pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) { + for r in refs { + assert!( + r.start + r.length <= bytecode.len(), + "immutable ref out of bounds: start={} length={} bytecode_len={}", + r.start, + r.length, + bytecode.len() + ); + bytecode[r.start..r.start + r.length].copy_from_slice(value); + } +} + +/// Convenience: patch with an ABI-encoded `address` (left-padded to 32 bytes). +pub(crate) fn patch_address(bytecode: &mut [u8], refs: &[ImmutableRef], addr: Address) { + let word: B256 = B256::from(U256::from_be_bytes(addr.into_word().0)); + patch_bytes(bytecode, refs, &word.0); +} + +/// Convenience: patch with an ABI-encoded `uint32` (left-padded to 32 bytes). +pub(crate) fn patch_u32(bytecode: &mut [u8], refs: &[ImmutableRef], val: u32) { + let word = B256::from(U256::from(val)); + patch_bytes(bytecode, refs, &word.0); +} + +/// Convenience: patch with an ABI-encoded `uint256`. +pub(crate) fn patch_u256(bytecode: &mut [u8], refs: &[ImmutableRef], val: U256) { + let word = B256::from(val); + patch_bytes(bytecode, refs, &word.0); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn patch_single_ref() { + let mut bytecode = vec![0u8; 64]; + let refs = [ImmutableRef { + start: 10, + length: 32, + }]; + let value = B256::from(U256::from(42u64)); + patch_bytes(&mut bytecode, &refs, &value.0); + + assert_eq!(bytecode[41], 42); + // bytes before are untouched + assert_eq!(bytecode[9], 0); + // bytes after are untouched + assert_eq!(bytecode[42], 0); + } + + #[test] + fn patch_multiple_refs() { + let mut bytecode = vec![0u8; 128]; + let refs = [ + ImmutableRef { + start: 0, + length: 32, + }, + ImmutableRef { + start: 64, + length: 32, + }, + ]; + let addr = Address::repeat_byte(0xAB); + patch_address(&mut bytecode, &refs, addr); + + // Both locations should have the address (last 20 bytes of the 32-byte word) + assert_eq!(bytecode[12..32], [0xAB; 20]); + assert_eq!(bytecode[76..96], [0xAB; 20]); + // Padding bytes should be zero + assert_eq!(bytecode[0..12], [0u8; 12]); + assert_eq!(bytecode[64..76], [0u8; 12]); + } + + #[test] + fn patch_u32_value() { + let mut bytecode = vec![0u8; 64]; + let refs = [ImmutableRef { + start: 0, + length: 32, + }]; + patch_u32(&mut bytecode, &refs, 1234); + + // uint32 1234 = 0x04D2, left-padded to 32 bytes + assert_eq!(bytecode[30], 0x04); + assert_eq!(bytecode[31], 0xD2); + assert_eq!(bytecode[0..30], [0u8; 30]); + } + + #[test] + #[should_panic(expected = "immutable ref out of bounds")] + fn patch_out_of_bounds_panics() { + let mut bytecode = vec![0u8; 16]; + let refs = [ImmutableRef { + start: 0, + length: 32, + }]; + let value = [0u8; 32]; + patch_bytes(&mut bytecode, &refs, &value); + } +} diff --git a/bin/ev-deployer/src/contracts/mailbox.rs b/bin/ev-deployer/src/contracts/mailbox.rs new file mode 100644 index 00000000..4f774587 --- /dev/null +++ b/bin/ev-deployer/src/contracts/mailbox.rs @@ -0,0 +1,295 @@ +//! `Mailbox` bytecode and storage encoding. +//! +//! `Mailbox` is the core Hyperlane messaging hub. It dispatches and processes +//! cross-chain messages. +//! +//! ## Immutables (in bytecode, not storage) +//! +//! | Variable | Type | Offsets | +//! |-----------------|---------|----------------------| +//! | `deployedBlock` | uint256 | \[930\] | +//! | `localDomain` | uint32 | \[982, 2831, 5985\] | +//! +//! ## Storage layout (from `forge inspect Mailbox storageLayout`) +//! +//! | Slot | Variable | Type | +//! |-------|-----------------------------|-----------| +//! | 0 | `_initialized` + `_initializing` | uint8 + bool | +//! | 1-50 | `__gap` (Initializable) | — | +//! | 51 | `_owner` | address | +//! | 52-100| `__gap` (Ownable) | — | +//! | 101 | `nonce` | uint32 | +//! | 102 | `latestDispatchedId` | bytes32 | +//! | 103 | `defaultIsm` | address | +//! | 104 | `defaultHook` | address | +//! | 105 | `requiredHook` | address | +//! | 106 | `deliveries` (mapping) | — | + +use crate::{ + config::MailboxConfig, + contracts::{ + immutables::{patch_u256, patch_u32, ImmutableRef}, + GenesisContract, + }, +}; +use alloy_primitives::{hex, Bytes, B256, U256}; +use std::collections::BTreeMap; + +/// `Mailbox` runtime bytecode compiled with Hyperlane v11.0.3, +/// solc 0.8.22 (Foundry `ci` profile: `cbor_metadata=false`, `bytecode_hash="none"`). +/// +/// Compiled with placeholder immutables (all zeros). Actual values are patched +/// at genesis time via [`build`]. +/// +/// Regenerate with: +/// ```sh +/// cd contracts/lib/hyperlane-monorepo/solidity && \ +/// forge soldeer install && \ +/// FOUNDRY_PROFILE=ci forge inspect Mailbox deployedBytecode +/// ``` +const MAILBOX_BYTECODE: &[u8] = &hex!("6080604052600436106101ac5760003560e01c80638da5cb5b116100ec578063e70f48ac1161008a578063f7ccd32111610064578063f7ccd321146105d7578063f8c8765e146105f7578063fa31de0114610617578063ffa1ad741461062a57600080fd5b8063e70f48ac14610577578063f2fde38b14610597578063f794687a146105b757600080fd5b80639c42bd18116100c65780639c42bd18146104ae578063affed0e0146104ce578063d6d08a09146104eb578063e495f1d41461051857600080fd5b80638da5cb5b1461040d57806393c448471461043857806399b048091461048e57600080fd5b80635d1fe5a9116101595780637c39d130116101335780637c39d1301461035d57806381d2ea951461037057806382ea7bfe146103905780638d3638f4146103c457600080fd5b80635d1fe5a9146102d85780636e5f516e1461031b578063715018a61461034857600080fd5b80631426b7f41161018a5780631426b7f4146102515780633d1250b71461027357806348aee8d4146102c557600080fd5b806307a2fda1146101b157806310b83dc01461021a578063134fbb4f1461023b575b600080fd5b3480156101bd57600080fd5b506101fe6101cc366004611b6a565b6000908152606a602052604090205474010000000000000000000000000000000000000000900465ffffffffffff1690565b60405165ffffffffffff90911681526020015b60405180910390f35b61022d610228366004611c00565b610651565b604051908152602001610211565b34801561024757600080fd5b5061022d60665481565b34801561025d57600080fd5b5061027161026c366004611c9e565b610925565b005b34801561027f57600080fd5b506068546102a09073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610211565b61022d6102d3366004611cbb565b610a45565b3480156102e457600080fd5b506102a06102f3366004611b6a565b6000908152606a602052604090205473ffffffffffffffffffffffffffffffffffffffff1690565b34801561032757600080fd5b506067546102a09073ffffffffffffffffffffffffffffffffffffffff1681565b34801561035457600080fd5b50610271610a83565b61027161036b366004611d45565b610a97565b34801561037c57600080fd5b5061022d61038b366004611c00565b610f39565b34801561039c57600080fd5b5061022d7f000000000000000000000000000000000000000000000000000000000000000081565b3480156103d057600080fd5b506103f87f000000000000000000000000000000000000000000000000000000000000000081565b60405163ffffffff9091168152602001610211565b34801561041957600080fd5b5060335473ffffffffffffffffffffffffffffffffffffffff166102a0565b34801561044457600080fd5b506104816040518060400160405280600681526020017f31312e302e33000000000000000000000000000000000000000000000000000081525081565b6040516102119190611e1f565b34801561049a57600080fd5b506102716104a9366004611c9e565b6110cc565b3480156104ba57600080fd5b5061022d6104c9366004611e32565b6111e7565b3480156104da57600080fd5b506065546103f89063ffffffff1681565b3480156104f757600080fd5b506069546102a09073ffffffffffffffffffffffffffffffffffffffff1681565b34801561052457600080fd5b50610567610533366004611b6a565b6000908152606a602052604090205474010000000000000000000000000000000000000000900465ffffffffffff16151590565b6040519015158152602001610211565b34801561058357600080fd5b506102a0610592366004611c9e565b611223565b3480156105a357600080fd5b506102716105b2366004611c9e565b61135a565b3480156105c357600080fd5b506102716105d2366004611c9e565b611411565b3480156105e357600080fd5b5061022d6105f2366004611cbb565b61152c565b34801561060357600080fd5b50610271610612366004611e80565b61155f565b61022d610625366004611e32565b611719565b34801561063657600080fd5b5061063f600381565b60405160ff9091168152602001610211565b600073ffffffffffffffffffffffffffffffffffffffff821661068a5760685473ffffffffffffffffffffffffffffffffffffffff1691505b60006106988989898961174c565b805160208201206066819055606580549293509091600191906000906106c590849063ffffffff16611f0b565b92506101000a81548163ffffffff021916908363ffffffff160217905550888a63ffffffff163373ffffffffffffffffffffffffffffffffffffffff167f769f711d20c679153d382254f59892613b58a97cc876b249134ac25c80f9c814856040516107319190611e1f565b60405180910390a460405181907f788dbc1b7152732178210e7f4d9d010ef016f9eafbe66786bd7169f56e0c353a90600090a26069546040517faaccd23000000000000000000000000000000000000000000000000000000000815260009173ffffffffffffffffffffffffffffffffffffffff169063aaccd230906107bf908a908a908890600401611f78565b602060405180830381865afa1580156107dc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108009190611fa8565b90508034101561080d5750345b6069546040517f086011b900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091169063086011b9908390610869908b908b908990600401611f78565b6000604051808303818588803b15801561088257600080fd5b505af1158015610896573d6000803e3d6000fd5b50505050508473ffffffffffffffffffffffffffffffffffffffff1663086011b982346108c39190611fc1565b8989876040518563ffffffff1660e01b81526004016108e493929190611f78565b6000604051808303818588803b1580156108fd57600080fd5b505af1158015610911573d6000803e3d6000fd5b50949e9d5050505050505050505050505050565b61092d611795565b73ffffffffffffffffffffffffffffffffffffffff81163b6109d6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f4d61696c626f783a20726571756972656420686f6f6b206e6f7420636f6e747260448201527f616374000000000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b606980547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517f329ec8e2438a73828ecf31a6568d7a91d7b1d79e342b0692914fd053d1a002b190600090a250565b6000610a78878787878787606860009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16610651565b979650505050505050565b610a8b611795565b610a956000611816565b565b6003610aa3838361188d565b60ff1614610b0d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f4d61696c626f783a206261642076657273696f6e00000000000000000000000060448201526064016109cd565b7f000000000000000000000000000000000000000000000000000000000000000063ffffffff16610b3e83836118b1565b63ffffffff1614610bab576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f4d61696c626f783a20756e65787065637465642064657374696e6174696f6e0060448201526064016109cd565b6000610bec83838080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061178a92505050565b6000818152606a602052604090205490915074010000000000000000000000000000000000000000900465ffffffffffff1615610c85576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d61696c626f783a20616c72656164792064656c69766572656400000000000060448201526064016109cd565b6000610c9184846118d4565b90506000610c9e82611223565b60408051808201825233815265ffffffffffff43811660208084019182526000898152606a9091529390932091518254935190911674010000000000000000000000000000000000000000027fffffffffffff000000000000000000000000000000000000000000000000000090931673ffffffffffffffffffffffffffffffffffffffff918216179290921790559091508216610d3c86866118ef565b610d468787611908565b63ffffffff167f0d381c2a574ae8f04e213db7cfb4df8df712cdbd427d9868ffef380660ca657460405160405180910390a460405183907f1cae38cdd3d3919489272725a5ae62a4f48b2989b0dae843d3c279fee18073a990600090a26040517ff7e83aee00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82169063f7e83aee90610dfb908a908a908a908a90600401611fd4565b6020604051808303816000875af1158015610e1a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e3e9190611ffb565b610ea4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4d61696c626f783a2049534d20766572696669636174696f6e206661696c656460448201526064016109cd565b8173ffffffffffffffffffffffffffffffffffffffff166356d5d47534610ecb8888611908565b610ed589896118ef565b610edf8a8a611918565b6040518663ffffffff1660e01b8152600401610efe949392919061201d565b6000604051808303818588803b158015610f1757600080fd5b505af1158015610f2b573d6000803e3d6000fd5b505050505050505050505050565b600073ffffffffffffffffffffffffffffffffffffffff8216610f725760685473ffffffffffffffffffffffffffffffffffffffff1691505b6000610f808989898961174c565b6040517faaccd23000000000000000000000000000000000000000000000000000000000815290915073ffffffffffffffffffffffffffffffffffffffff84169063aaccd23090610fd990889088908690600401611f78565b602060405180830381865afa158015610ff6573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061101a9190611fa8565b6069546040517faaccd23000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091169063aaccd2309061107490899089908790600401611f78565b602060405180830381865afa158015611091573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110b59190611fa8565b6110bf9190612043565b9998505050505050505050565b6110d4611795565b73ffffffffffffffffffffffffffffffffffffffff81163b611178576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f4d61696c626f783a2064656661756c7420686f6f6b206e6f7420636f6e74726160448201527f637400000000000000000000000000000000000000000000000000000000000060648201526084016109cd565b606880547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517f65a63e5066ee2fcdf9d32a7f1bf7ce71c76066f19d0609dddccd334ab87237d790600090a250565b600061121a858585856111fc86808385612056565b60685473ffffffffffffffffffffffffffffffffffffffff16610f39565b95945050505050565b60408051600481526024810182526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fde523cf30000000000000000000000000000000000000000000000000000000017905290516000918291829173ffffffffffffffffffffffffffffffffffffffff8616916112a49190612080565b600060405180830381855afa9150503d80600081146112df576040519150601f19603f3d011682016040523d82523d6000602084013e6112e4565b606091505b50915091508180156112f65750805115155b1561133957600081806020019051810190611311919061209c565b905073ffffffffffffffffffffffffffffffffffffffff81161561133757949350505050565b505b505060675473ffffffffffffffffffffffffffffffffffffffff1692915050565b611362611795565b73ffffffffffffffffffffffffffffffffffffffff8116611405576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016109cd565b61140e81611816565b50565b611419611795565b73ffffffffffffffffffffffffffffffffffffffff81163b6114bd576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f4d61696c626f783a2064656661756c742049534d206e6f7420636f6e7472616360448201527f740000000000000000000000000000000000000000000000000000000000000060648201526084016109cd565b606780547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040517fa76ad0adbf45318f8633aa0210f711273d50fbb6fef76ed95bbae97082c75daa90600090a250565b6000610a78878787878787606860009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16610f39565b600054610100900460ff161580801561157f5750600054600160ff909116105b806115995750303b158015611599575060005460ff166001145b611625576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a656400000000000000000000000000000000000060648201526084016109cd565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055801561168357600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b61168b611934565b61169484611411565b61169d836110cc565b6116a682610925565b6116af8561135a565b801561171257600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050505050565b600061121a8585858561172e86808385612056565b60685473ffffffffffffffffffffffffffffffffffffffff16610651565b60655460609061121a9060039063ffffffff167f000000000000000000000000000000000000000000000000000000000000000033898989896119d3565b805160209091012090565b60335473ffffffffffffffffffffffffffffffffffffffff163314610a95576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016109cd565b6033805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b600061189c6001828486612056565b6118a5916120b9565b60f81c90505b92915050565b60006118c1602d60298486612056565b6118ca91612101565b60e01c9392505050565b60006118e86118e38484611a11565b611a21565b9392505050565b60006118ff602960098486612056565b6118e891612147565b60006118c1600960058486612056565b36600061192883604d8187612056565b915091505b9250929050565b600054610100900460ff166119cb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016109cd565b610a95611aca565b606088888888888888886040516020016119f4989796959493929190612183565b604051602081830303815290604052905098975050505050505050565b60006118ff604d602d8486612056565b600073ffffffffffffffffffffffffffffffffffffffff821115611ac6576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f5479706543617374733a2062797465733332546f41646472657373206f76657260448201527f666c6f770000000000000000000000000000000000000000000000000000000060648201526084016109cd565b5090565b600054610100900460ff16611b61576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016109cd565b610a9533611816565b600060208284031215611b7c57600080fd5b5035919050565b803563ffffffff81168114611b9757600080fd5b919050565b60008083601f840112611bae57600080fd5b50813567ffffffffffffffff811115611bc657600080fd5b60208301915083602082850101111561192d57600080fd5b73ffffffffffffffffffffffffffffffffffffffff8116811461140e57600080fd5b600080600080600080600060a0888a031215611c1b57600080fd5b611c2488611b83565b965060208801359550604088013567ffffffffffffffff80821115611c4857600080fd5b611c548b838c01611b9c565b909750955060608a0135915080821115611c6d57600080fd5b50611c7a8a828b01611b9c565b9094509250506080880135611c8e81611bde565b8091505092959891949750929550565b600060208284031215611cb057600080fd5b81356118e881611bde565b60008060008060008060808789031215611cd457600080fd5b611cdd87611b83565b955060208701359450604087013567ffffffffffffffff80821115611d0157600080fd5b611d0d8a838b01611b9c565b90965094506060890135915080821115611d2657600080fd5b50611d3389828a01611b9c565b979a9699509497509295939492505050565b60008060008060408587031215611d5b57600080fd5b843567ffffffffffffffff80821115611d7357600080fd5b611d7f88838901611b9c565b90965094506020870135915080821115611d9857600080fd5b50611da587828801611b9c565b95989497509550505050565b60005b83811015611dcc578181015183820152602001611db4565b50506000910152565b60008151808452611ded816020860160208601611db1565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b6020815260006118e86020830184611dd5565b60008060008060608587031215611e4857600080fd5b611e5185611b83565b935060208501359250604085013567ffffffffffffffff811115611e7457600080fd5b611da587828801611b9c565b60008060008060808587031215611e9657600080fd5b8435611ea181611bde565b93506020850135611eb181611bde565b92506040850135611ec181611bde565b91506060850135611ed181611bde565b939692955090935050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b63ffffffff818116838216019080821115611f2857611f28611edc565b5092915050565b8183528181602085013750600060208284010152600060207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b604081526000611f8c604083018587611f2f565b8281036020840152611f9e8185611dd5565b9695505050505050565b600060208284031215611fba57600080fd5b5051919050565b818103818111156118ab576118ab611edc565b604081526000611fe8604083018688611f2f565b8281036020840152610a78818587611f2f565b60006020828403121561200d57600080fd5b815180151581146118e857600080fd5b63ffffffff85168152836020820152606060408201526000611f9e606083018486611f2f565b808201808211156118ab576118ab611edc565b6000808585111561206657600080fd5b8386111561207357600080fd5b5050820193919092039150565b60008251612092818460208701611db1565b9190910192915050565b6000602082840312156120ae57600080fd5b81516118e881611bde565b7fff0000000000000000000000000000000000000000000000000000000000000081358181169160018510156120f95780818660010360031b1b83161692505b505092915050565b7fffffffff0000000000000000000000000000000000000000000000000000000081358181169160048510156120f95760049490940360031b84901b1690921692915050565b803560208310156118ab577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff602084900360031b1b1692915050565b7fff000000000000000000000000000000000000000000000000000000000000008960f81b16815260007fffffffff00000000000000000000000000000000000000000000000000000000808a60e01b166001840152808960e01b166005840152876009840152808760e01b1660298401525084602d8301528284604d8401375060009101604d0190815297965050505050505056"); + +// ── Immutable reference offsets (from `forge inspect Mailbox immutableReferences`) ── + +/// `deployedBlock` (uint256) — from `Indexed.sol`. Set to 0 for genesis contracts. +const DEPLOYED_BLOCK_REFS: &[ImmutableRef] = &[ImmutableRef { + start: 930, + length: 32, +}]; + +/// `localDomain` (uint32) — from `Mailbox.sol`. +const LOCAL_DOMAIN_REFS: &[ImmutableRef] = &[ + ImmutableRef { + start: 982, + length: 32, + }, + ImmutableRef { + start: 2831, + length: 32, + }, + ImmutableRef { + start: 5985, + length: 32, + }, +]; + +/// Build a genesis alloc entry for `Mailbox`. +pub(crate) fn build(config: &MailboxConfig, local_domain: u32) -> GenesisContract { + let mut bytecode = MAILBOX_BYTECODE.to_vec(); + + // Patch immutables + patch_u256(&mut bytecode, DEPLOYED_BLOCK_REFS, U256::ZERO); + patch_u32(&mut bytecode, LOCAL_DOMAIN_REFS, local_domain); + + let mut storage = BTreeMap::new(); + + // Slot 0: _initialized = 1 (OZ v4 Initializable), _initializing = false + storage.insert(B256::ZERO, B256::from(U256::from(1u8))); + + // Slot 51: _owner + if !config.owner.is_zero() { + storage.insert( + B256::from(U256::from(51u64)), + B256::from(U256::from_be_bytes(config.owner.into_word().0)), + ); + } + + // Slot 103: defaultIsm + if !config.default_ism.is_zero() { + storage.insert( + B256::from(U256::from(103u64)), + B256::from(U256::from_be_bytes(config.default_ism.into_word().0)), + ); + } + + // Slot 104: defaultHook + if !config.default_hook.is_zero() { + storage.insert( + B256::from(U256::from(104u64)), + B256::from(U256::from_be_bytes(config.default_hook.into_word().0)), + ); + } + + // Slot 105: requiredHook + if !config.required_hook.is_zero() { + storage.insert( + B256::from(U256::from(105u64)), + B256::from(U256::from_be_bytes(config.required_hook.into_word().0)), + ); + } + + GenesisContract { + address: config.address, + code: Bytes::from(bytecode), + storage, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, hex, Address}; + use std::{path::PathBuf, process::Command}; + + fn test_config() -> MailboxConfig { + MailboxConfig { + address: address!("0000000000000000000000000000000000001200"), + owner: address!("000000000000000000000000000000000000ad00"), + default_ism: address!("0000000000000000000000000000000000002000"), + default_hook: address!("0000000000000000000000000000000000003000"), + required_hook: address!("0000000000000000000000000000000000004000"), + } + } + + #[test] + fn storage_has_initialized_flag() { + let contract = build(&test_config(), 1234); + assert_eq!( + contract.storage[&B256::ZERO], + B256::from(U256::from(1u8)), + "_initialized should be 1" + ); + } + + #[test] + fn storage_has_owner() { + let contract = build(&test_config(), 1234); + let owner_slot = B256::from(U256::from(51u64)); + let expected: B256 = "0x000000000000000000000000000000000000000000000000000000000000Ad00" + .parse() + .unwrap(); + assert_eq!(contract.storage[&owner_slot], expected); + } + + #[test] + fn storage_has_default_ism() { + let contract = build(&test_config(), 1234); + let slot = B256::from(U256::from(103u64)); + let expected: B256 = "0x0000000000000000000000000000000000000000000000000000000000002000" + .parse() + .unwrap(); + assert_eq!(contract.storage[&slot], expected); + } + + #[test] + fn storage_has_default_hook() { + let contract = build(&test_config(), 1234); + let slot = B256::from(U256::from(104u64)); + let expected: B256 = "0x0000000000000000000000000000000000000000000000000000000000003000" + .parse() + .unwrap(); + assert_eq!(contract.storage[&slot], expected); + } + + #[test] + fn storage_has_required_hook() { + let contract = build(&test_config(), 1234); + let slot = B256::from(U256::from(105u64)); + let expected: B256 = "0x0000000000000000000000000000000000000000000000000000000000004000" + .parse() + .unwrap(); + assert_eq!(contract.storage[&slot], expected); + } + + #[test] + fn bytecode_is_patched_with_local_domain() { + let config = test_config(); + let contract = build(&config, 42); + let code = contract.code.to_vec(); + + for &offset in &[982, 2831, 5985] { + let word = &code[offset..offset + 32]; + assert_eq!(word[31], 42, "localDomain not patched at offset {offset}"); + assert_eq!( + word[0..31], + [0u8; 31], + "localDomain padding wrong at offset {offset}" + ); + } + } + + #[test] + fn bytecode_has_zero_deployed_block() { + let config = test_config(); + let contract = build(&config, 1234); + let code = contract.code.to_vec(); + + let word = &code[930..930 + 32]; + assert_eq!(word, &[0u8; 32], "deployedBlock should be 0 at genesis"); + } + + #[test] + fn storage_count_for_standard_config() { + let contract = build(&test_config(), 1234); + // _initialized (0) + _owner (51) + defaultIsm (103) + defaultHook (104) + requiredHook (105) + assert_eq!(contract.storage.len(), 5); + } + + #[test] + fn zero_addresses_omit_slots() { + let config = MailboxConfig { + address: address!("0000000000000000000000000000000000001200"), + owner: Address::ZERO, + default_ism: Address::ZERO, + default_hook: Address::ZERO, + required_hook: Address::ZERO, + }; + let contract = build(&config, 1234); + // Only _initialized (slot 0) should be present + assert_eq!(contract.storage.len(), 1); + assert!(!contract + .storage + .contains_key(&B256::from(U256::from(51u64)))); + assert!(!contract + .storage + .contains_key(&B256::from(U256::from(103u64)))); + assert!(!contract + .storage + .contains_key(&B256::from(U256::from(104u64)))); + assert!(!contract + .storage + .contains_key(&B256::from(U256::from(105u64)))); + } + + #[test] + #[ignore = "requires forge CLI"] + fn mailbox_bytecode_matches_solidity_source() { + let contracts_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .unwrap() + .join("contracts") + .join("lib") + .join("hyperlane-monorepo") + .join("solidity"); + + let output = Command::new("forge") + .args(["inspect", "Mailbox", "deployedBytecode", "--root"]) + .arg(&contracts_root) + .env("FOUNDRY_PROFILE", "ci") + .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() + .strip_prefix("0x") + .unwrap() + .to_lowercase(); + + let hardcoded_hex = hex::encode(MAILBOX_BYTECODE); + + assert_eq!( + forge_hex, hardcoded_hex, + "Mailbox bytecode mismatch! Regenerate with: \ + cd contracts/lib/hyperlane-monorepo/solidity && \ + FOUNDRY_PROFILE=ci forge inspect Mailbox deployedBytecode" + ); + } +} diff --git a/bin/ev-deployer/src/contracts/merkle_tree_hook.rs b/bin/ev-deployer/src/contracts/merkle_tree_hook.rs new file mode 100644 index 00000000..0723adab --- /dev/null +++ b/bin/ev-deployer/src/contracts/merkle_tree_hook.rs @@ -0,0 +1,254 @@ +//! `MerkleTreeHook` bytecode and storage encoding. +//! +//! `MerkleTreeHook` is a Hyperlane post-dispatch hook that maintains an +//! incremental Merkle tree of dispatched message IDs. Validators sign +//! checkpoints against this tree to attest to the messages. +//! +//! ## Immutables (in bytecode, not storage) +//! +//! | Variable | Type | Offsets | +//! |---------------|---------|---------------------| +//! | `mailbox` | address | \[904, 3300\] | +//! | `localDomain` | uint32 | \[644\] | +//! | `deployedBlock`| uint256| \[578\] | +//! +//! ## Storage layout (from `forge inspect MerkleTreeHook storageLayout`) +//! +//! | Slot | Variable | Type | +//! |------|-----------------------------|---------| +//! | 0 | `_initialized` + `_initializing` | uint8 + bool | +//! | 1-50 | `__gap` (Initializable) | — | +//! | 51 | `_owner` | address | +//! | 52-100| `__gap` (Ownable) | — | +//! | 101 | `hook` | address | +//! | 102 | `_interchainSecurityModule` | address | +//! | 103-150| `__GAP` (MailboxClient) | — | +//! | 151-182| `_tree.branch\[0..31\]` | bytes32\[32\] | +//! | 183 | `_tree.count` | uint256 | + +use crate::{ + config::MerkleTreeHookConfig, + contracts::{ + immutables::{patch_address, patch_u256, patch_u32, ImmutableRef}, + GenesisContract, + }, +}; +use alloy_primitives::{hex, Bytes, B256, U256}; +use std::collections::BTreeMap; + +/// `MerkleTreeHook` runtime bytecode compiled with Hyperlane v11.0.3, +/// solc 0.8.22 (Foundry `ci` profile: `cbor_metadata=false`, `bytecode_hash="none"`). +/// +/// Compiled with placeholder immutables (all zeros). Actual values are patched +/// at genesis time via [`build`]. +/// +/// Regenerate with: +/// ```sh +/// cd contracts/lib/hyperlane-monorepo/solidity && \ +/// forge soldeer install && \ +/// FOUNDRY_PROFILE=ci forge inspect MerkleTreeHook deployedBytecode +/// ``` +const MERKLE_TREE_HOOK_BYTECODE: &[u8] = &hex!("6080604052600436106101445760003560e01c8063907c0f92116100c0578063e445e7dd11610074578063ebf0c71711610059578063ebf0c7171461042c578063f2fde38b14610441578063fd54b2281461046157600080fd5b8063e445e7dd146103d5578063e5320bb9146103fc57600080fd5b8063aaccd230116100a5578063aaccd23014610356578063d5438eae14610376578063de523cf3146103aa57600080fd5b8063907c0f92146102d157806393c448471461030057600080fd5b8063715018a61161011757806382ea7bfe116100fc57806382ea7bfe146102305780638d3638f4146102725780638da5cb5b146102a657600080fd5b8063715018a6146101c95780637f5a7c7b146101de57600080fd5b806306661abd14610149578063086011b9146101745780630e72cc06146101895780633dfd3873146101a9575b600080fd5b34801561015557600080fd5b5060b7545b60405163ffffffff90911681526020015b60405180910390f35b6101876101823660046114c2565b610483565b005b34801561019557600080fd5b506101876101a436600461152e565b610530565b3480156101b557600080fd5b506101876101c436600461152e565b610679565b3480156101d557600080fd5b506101876107ba565b3480156101ea57600080fd5b5060655461020b9073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200161016b565b34801561023c57600080fd5b506102647f000000000000000000000000000000000000000000000000000000000000000081565b60405190815260200161016b565b34801561027e57600080fd5b5061015a7f000000000000000000000000000000000000000000000000000000000000000081565b3480156102b257600080fd5b5060335473ffffffffffffffffffffffffffffffffffffffff1661020b565b3480156102dd57600080fd5b506102e66107ce565b6040805192835263ffffffff90911660208301520161016b565b34801561030c57600080fd5b506103496040518060400160405280600681526020017f31312e302e33000000000000000000000000000000000000000000000000000081525081565b60405161016b919061156b565b34801561036257600080fd5b506102646103713660046114c2565b6107f6565b34801561038257600080fd5b5061020b7f000000000000000000000000000000000000000000000000000000000000000081565b3480156103b657600080fd5b5060665473ffffffffffffffffffffffffffffffffffffffff1661020b565b3480156103e157600080fd5b506103ea610899565b60405160ff909116815260200161016b565b34801561040857600080fd5b5061041c6104173660046115d8565b6108a3565b604051901515815260200161016b565b34801561043857600080fd5b506102646108c8565b34801561044d57600080fd5b5061018761045c36600461152e565b6108d4565b34801561046d57600080fd5b5061047661098b565b60405161016b919061161a565b61048d84846108a3565b61051e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603260248201527f4162737472616374506f73744469737061746368486f6f6b3a20696e76616c6960448201527f64206d657461646174612076617269616e74000000000000000000000000000060648201526084015b60405180910390fd5b61052a848484846109da565b50505050565b8073ffffffffffffffffffffffffffffffffffffffff81163b15158061056a575073ffffffffffffffffffffffffffffffffffffffff8116155b6105f6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f4d61696c626f78436c69656e743a20696e76616c696420636f6e74726163742060448201527f73657474696e67000000000000000000000000000000000000000000000000006064820152608401610515565b6105fe610b78565b606680547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff84169081179091556040519081527fc47cbcc588c67679e52261c45cc315e56562f8d0ccaba16facb9093ff9498799906020015b60405180910390a15050565b8073ffffffffffffffffffffffffffffffffffffffff81163b1515806106b3575073ffffffffffffffffffffffffffffffffffffffff8116155b61073f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f4d61696c626f78436c69656e743a20696e76616c696420636f6e74726163742060448201527f73657474696e67000000000000000000000000000000000000000000000000006064820152608401610515565b610747610b78565b606580547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff84169081179091556040519081527f4eab7b127c764308788622363ad3e9532de3dfba7845bd4f84c125a22544255a9060200161066d565b6107c2610b78565b6107cc6000610bf9565b565b6000806107d96108c8565b60016107e460b75490565b6107ee919061168c565b915091509091565b600061080285856108a3565b61088e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603260248201527f4162737472616374506f73744469737061746368486f6f6b3a20696e76616c6960448201527f64206d657461646174612076617269616e7400000000000000000000000000006064820152608401610515565b600095945050505050565b600060035b905090565b60008115806108bf575060016108b98484610c70565b61ffff16145b90505b92915050565b600061089e6097610cc1565b6108dc610b78565b73ffffffffffffffffffffffffffffffffffffffff811661097f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f64647265737300000000000000000000000000000000000000000000000000006064820152608401610515565b61098881610bf9565b50565b61099361143a565b60408051610440810180835290916097918391820190839060209082845b8154815260200190600101908083116109b1575050509183525050602091820154910152919050565b3415610a68576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f4d65726b6c6554726565486f6f6b3a206e6f2076616c7565206578706563746560448201527f64000000000000000000000000000000000000000000000000000000000000006064820152608401610515565b6000610aa983838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610cd492505050565b9050610ab481610cdf565b610b1a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f6d657373616765206e6f74206469737061746368696e670000000000000000006044820152606401610515565b6000610b2560b75490565b9050610b32609783610d78565b6040805183815263ffffffff831660208201527f253a3a04cab70d47c1504809242d9350cd81627b4f1d50753e159cf8cd76ed33910160405180910390a1505050505050565b60335473ffffffffffffffffffffffffffffffffffffffff1633146107cc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610515565b6033805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6000610c7d8160026116b0565b60ff16821015610c8f575060006108c2565b82600083610c9e8260026116b0565b60ff1692610cae939291906116c9565b610cb7916116f3565b60f01c9392505050565b60006108c282610ccf610eb2565b611373565b805160209091012090565b6000817f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663134fbb4f6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d4d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d719190611739565b1492915050565b6001610d8660206002611872565b610d90919061187e565b826020015410610dfc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601060248201527f6d65726b6c6520747265652066756c6c000000000000000000000000000000006044820152606401610515565b6001826020016000828254610e119190611891565b9091555050602082015460005b6020811015610ea45781600116600103610e4d5782848260208110610e4557610e456118a4565b015550505050565b838160208110610e5f57610e5f6118a4565b01546040805160208101929092528101849052606001604051602081830303815290604052805190602001209250600282610e9a91906118d3565b9150600101610e1e565b50610ead61190e565b505050565b610eba61145a565b600081527fad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb560208201527fb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3060408201527f21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba8560608201527fe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a1934460808201527f0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d60a08201527f887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a196860c08201527fffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f8360e08201527f9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af6101008201527fcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e06101208201527ff9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a56101408201527ff8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf8926101608201527f3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c6101808201527fc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb6101a08201527f5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc6101c08201527fda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d26101e08201527f2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f6102008201527fe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a6102208201527f5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a06102408201527fb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa06102608201527fc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e26102808201527ff4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd96102a08201527f5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3776102c08201527f4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee6526102e08201527fcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef6103008201527f0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d6103208201527fb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d06103408201527f838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e6103608201527f662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e6103808201527f388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea3226103a08201527f93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d7356103c08201527f8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a96103e082015290565b6020820154600090815b602081101561143257600182821c1660008683602081106113a0576113a06118a4565b01549050816001036113dd576040805160208101839052908101869052606001604051602081830303815290604052805190602001209450611428565b848684602081106113f0576113f06118a4565b602002015160405160200161140f929190918252602082015260400190565b6040516020818303038152906040528051906020012094505b505060010161137d565b505092915050565b604051806040016040528061144d61145a565b8152602001600081525090565b6040518061040001604052806020906020820280368337509192915050565b60008083601f84011261148b57600080fd5b50813567ffffffffffffffff8111156114a357600080fd5b6020830191508360208285010111156114bb57600080fd5b9250929050565b600080600080604085870312156114d857600080fd5b843567ffffffffffffffff808211156114f057600080fd5b6114fc88838901611479565b9096509450602087013591508082111561151557600080fd5b5061152287828801611479565b95989497509550505050565b60006020828403121561154057600080fd5b813573ffffffffffffffffffffffffffffffffffffffff8116811461156457600080fd5b9392505050565b60006020808352835180602085015260005b818110156115995785810183015185820160400152820161157d565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b600080602083850312156115eb57600080fd5b823567ffffffffffffffff81111561160257600080fd5b61160e85828601611479565b90969095509350505050565b81516104208201908260005b60208082106116355750611649565b835183529283019290910190600101611626565b505050602083015161040083015292915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b63ffffffff8281168282160390808211156116a9576116a961165d565b5092915050565b60ff81811683821601908111156108c2576108c261165d565b600080858511156116d957600080fd5b838611156116e657600080fd5b5050820193919092039150565b7fffff00000000000000000000000000000000000000000000000000000000000081358181169160028510156114325760029490940360031b84901b1690921692915050565b60006020828403121561174b57600080fd5b5051919050565b600181815b808511156117ab57817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156117915761179161165d565b8085161561179e57918102915b93841c9390800290611757565b509250929050565b6000826117c2575060016108c2565b816117cf575060006108c2565b81600181146117e557600281146117ef5761180b565b60019150506108c2565b60ff8411156118005761180061165d565b50506001821b6108c2565b5060208310610133831016604e8410600b841016171561182e575081810a6108c2565b6118388383611752565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0482111561186a5761186a61165d565b029392505050565b60006108bf83836117b3565b818103818111156108c2576108c261165d565b808201808211156108c2576108c261165d565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082611909577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd"); + +// ── Immutable reference offsets (from `forge inspect MerkleTreeHook immutableReferences`) ── + +/// `deployedBlock` (uint256) — from `Indexed.sol`. Set to 0 for genesis contracts. +const DEPLOYED_BLOCK_REFS: &[ImmutableRef] = &[ImmutableRef { + start: 578, + length: 32, +}]; + +/// `mailbox` (address) — from `MailboxClient.sol`. +const MAILBOX_REFS: &[ImmutableRef] = &[ + ImmutableRef { + start: 904, + length: 32, + }, + ImmutableRef { + start: 3300, + length: 32, + }, +]; + +/// `localDomain` (uint32) — from `MailboxClient.sol`. +const LOCAL_DOMAIN_REFS: &[ImmutableRef] = &[ImmutableRef { + start: 644, + length: 32, +}]; + +/// Build a genesis alloc entry for `MerkleTreeHook`. +pub(crate) fn build(config: &MerkleTreeHookConfig, local_domain: u32) -> GenesisContract { + let mut bytecode = MERKLE_TREE_HOOK_BYTECODE.to_vec(); + + // Patch immutables + patch_address(&mut bytecode, MAILBOX_REFS, config.mailbox); + patch_u32(&mut bytecode, LOCAL_DOMAIN_REFS, local_domain); + patch_u256(&mut bytecode, DEPLOYED_BLOCK_REFS, U256::ZERO); + + let mut storage = BTreeMap::new(); + + // Slot 0: _initialized = 1 (OZ v4 Initializable), _initializing = false + // byte layout: [_initialized (1 byte)] [_initializing (1 byte)] [... 30 zero bytes] + // Packed at slot 0: value = 0x01 (just _initialized = 1) + storage.insert(B256::ZERO, B256::from(U256::from(1u8))); + + // Slot 51: _owner — set the owner so the contract can be administered post-genesis + if !config.owner.is_zero() { + storage.insert( + B256::from(U256::from(51u64)), + B256::from(U256::from_be_bytes(config.owner.into_word().0)), + ); + } + + // All other storage starts at zero: + // - hook (slot 101): zero = use mailbox default + // - _interchainSecurityModule (slot 102): zero = use mailbox default + // - _tree.branch[0..31] (slots 151-182): all zero (empty tree) + // - _tree.count (slot 183): 0 + + GenesisContract { + address: config.address, + code: Bytes::from(bytecode), + storage, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, hex, Address}; + use std::{path::PathBuf, process::Command}; + + fn test_config() -> MerkleTreeHookConfig { + MerkleTreeHookConfig { + address: address!("0000000000000000000000000000000000001100"), + owner: address!("000000000000000000000000000000000000ad00"), + mailbox: address!("0000000000000000000000000000000000001200"), + } + } + + #[test] + fn storage_has_initialized_flag() { + let contract = build(&test_config(), 1234); + assert_eq!( + contract.storage[&B256::ZERO], + B256::from(U256::from(1u8)), + "_initialized should be 1" + ); + } + + #[test] + fn storage_has_owner() { + let contract = build(&test_config(), 1234); + let owner_slot = B256::from(U256::from(51u64)); + let expected: B256 = "0x000000000000000000000000000000000000000000000000000000000000Ad00" + .parse() + .unwrap(); + assert_eq!(contract.storage[&owner_slot], expected); + } + + #[test] + fn zero_owner_omits_slot() { + let config = MerkleTreeHookConfig { + address: address!("0000000000000000000000000000000000001100"), + owner: Address::ZERO, + mailbox: address!("0000000000000000000000000000000000001200"), + }; + let contract = build(&config, 1234); + let owner_slot = B256::from(U256::from(51u64)); + assert!( + !contract.storage.contains_key(&owner_slot), + "zero owner should not produce a storage entry" + ); + } + + #[test] + fn bytecode_is_patched_with_mailbox() { + let config = test_config(); + let contract = build(&config, 1234); + let code = contract.code.to_vec(); + + // Check that mailbox address is patched at both offsets + for &offset in &[904, 3300] { + let word = &code[offset..offset + 32]; + // Address is in the last 20 bytes of the 32-byte word + let addr_bytes = &word[12..32]; + assert_eq!( + addr_bytes, + config.mailbox.as_slice(), + "mailbox not patched at offset {offset}" + ); + } + } + + #[test] + fn bytecode_is_patched_with_local_domain() { + let config = test_config(); + let contract = build(&config, 42); + let code = contract.code.to_vec(); + + let word = &code[644..644 + 32]; + // uint32(42) in big-endian, left-padded: ...00 00 00 2a + assert_eq!(word[31], 42); + assert_eq!(word[0..31], [0u8; 31]); + } + + #[test] + fn bytecode_has_zero_deployed_block() { + let config = test_config(); + let contract = build(&config, 1234); + let code = contract.code.to_vec(); + + let word = &code[578..578 + 32]; + assert_eq!(word, &[0u8; 32], "deployedBlock should be 0 at genesis"); + } + + #[test] + fn only_two_storage_slots_for_standard_config() { + let contract = build(&test_config(), 1234); + // Should have exactly 2 storage entries: _initialized (slot 0) and _owner (slot 51) + assert_eq!(contract.storage.len(), 2); + } + + #[test] + #[ignore = "requires forge CLI"] + fn merkle_tree_hook_bytecode_matches_solidity_source() { + let contracts_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .unwrap() + .join("contracts") + .join("lib") + .join("hyperlane-monorepo") + .join("solidity"); + + let output = Command::new("forge") + .args(["inspect", "MerkleTreeHook", "deployedBytecode", "--root"]) + .arg(&contracts_root) + .env("FOUNDRY_PROFILE", "ci") + .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() + .strip_prefix("0x") + .unwrap() + .to_lowercase(); + + let hardcoded_hex = hex::encode(MERKLE_TREE_HOOK_BYTECODE); + + assert_eq!( + forge_hex, hardcoded_hex, + "MerkleTreeHook bytecode mismatch! Regenerate with: \ + cd contracts/lib/hyperlane-monorepo/solidity && \ + FOUNDRY_PROFILE=ci forge inspect MerkleTreeHook deployedBytecode" + ); + } +} diff --git a/bin/ev-deployer/src/contracts/mod.rs b/bin/ev-deployer/src/contracts/mod.rs index 8ef01558..d2c6500f 100644 --- a/bin/ev-deployer/src/contracts/mod.rs +++ b/bin/ev-deployer/src/contracts/mod.rs @@ -2,6 +2,11 @@ pub(crate) mod admin_proxy; pub(crate) mod fee_vault; +pub(crate) mod immutables; +pub(crate) mod mailbox; +pub(crate) mod merkle_tree_hook; +pub(crate) mod noop_ism; +pub(crate) mod protocol_fee; use alloy_primitives::{Address, Bytes, B256}; use std::collections::BTreeMap; diff --git a/bin/ev-deployer/src/contracts/noop_ism.rs b/bin/ev-deployer/src/contracts/noop_ism.rs new file mode 100644 index 00000000..9f17efe0 --- /dev/null +++ b/bin/ev-deployer/src/contracts/noop_ism.rs @@ -0,0 +1,109 @@ +//! `NoopIsm` bytecode encoding. +//! +//! `NoopIsm` is a Hyperlane Interchain Security Module (ISM) that accepts all +//! messages without verification — `verify` always returns `true`. +//! +//! ## Immutables +//! +//! None. +//! +//! ## Storage layout +//! +//! None. + +use crate::{config::NoopIsmConfig, contracts::GenesisContract}; +use alloy_primitives::{hex, Bytes}; +use std::collections::BTreeMap; + +/// `NoopIsm` runtime bytecode compiled with Hyperlane v11.0.3, +/// solc 0.8.22 (Foundry `ci` profile: `cbor_metadata=false`, `bytecode_hash="none"`). +/// +/// Regenerate with: +/// ```sh +/// cd contracts/lib/hyperlane-monorepo/solidity && \ +/// forge soldeer install && \ +/// FOUNDRY_PROFILE=ci forge inspect NoopIsm deployedBytecode +/// ``` +const NOOP_ISM_BYTECODE: &[u8] = &hex!("608060405234801561001057600080fd5b50600436106100415760003560e01c80636465e69f1461004657806393c4484714610065578063f7e83aee146100ae575b600080fd5b61004e600681565b60405160ff90911681526020015b60405180910390f35b6100a16040518060400160405280600681526020017f31312e302e33000000000000000000000000000000000000000000000000000081525081565b60405161005c91906100d6565b6100c66100bc36600461018c565b6001949350505050565b604051901515815260200161005c565b60006020808352835180602085015260005b81811015610104578581018301518582016040015282016100e8565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b60008083601f84011261015557600080fd5b50813567ffffffffffffffff81111561016d57600080fd5b60208301915083602082850101111561018557600080fd5b9250929050565b600080600080604085870312156101a257600080fd5b843567ffffffffffffffff808211156101ba57600080fd5b6101c688838901610143565b909650945060208701359150808211156101df57600080fd5b506101ec87828801610143565b9598949750955050505056"); + +/// Build a genesis alloc entry for `NoopIsm`. +pub(crate) fn build(config: &NoopIsmConfig) -> GenesisContract { + GenesisContract { + address: config.address, + code: Bytes::from(NOOP_ISM_BYTECODE.to_vec()), + storage: BTreeMap::new(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, hex}; + use std::{path::PathBuf, process::Command}; + + fn test_config() -> NoopIsmConfig { + NoopIsmConfig { + address: address!("0000000000000000000000000000000000001300"), + } + } + + #[test] + fn storage_is_empty() { + let contract = build(&test_config()); + assert!( + contract.storage.is_empty(), + "NoopIsm should have no storage" + ); + } + + #[test] + fn bytecode_is_present() { + let contract = build(&test_config()); + assert!( + !contract.code.is_empty(), + "NoopIsm should have non-empty bytecode" + ); + } + + #[test] + #[ignore = "requires forge CLI"] + fn noop_ism_bytecode_matches_solidity_source() { + let contracts_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .unwrap() + .join("contracts") + .join("lib") + .join("hyperlane-monorepo") + .join("solidity"); + + let output = Command::new("forge") + .args(["inspect", "NoopIsm", "deployedBytecode", "--root"]) + .arg(&contracts_root) + .env("FOUNDRY_PROFILE", "ci") + .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() + .strip_prefix("0x") + .unwrap() + .to_lowercase(); + + let hardcoded_hex = hex::encode(NOOP_ISM_BYTECODE); + + assert_eq!( + forge_hex, hardcoded_hex, + "NoopIsm bytecode mismatch! Regenerate with: \ + cd contracts/lib/hyperlane-monorepo/solidity && \ + FOUNDRY_PROFILE=ci forge inspect NoopIsm deployedBytecode" + ); + } +} diff --git a/bin/ev-deployer/src/contracts/protocol_fee.rs b/bin/ev-deployer/src/contracts/protocol_fee.rs new file mode 100644 index 00000000..d7b2135c --- /dev/null +++ b/bin/ev-deployer/src/contracts/protocol_fee.rs @@ -0,0 +1,209 @@ +//! `ProtocolFee` bytecode and storage encoding. +//! +//! `ProtocolFee` is a Hyperlane post-dispatch hook that charges a protocol fee +//! on message dispatches. +//! +//! ## Immutables (in bytecode, not storage) +//! +//! | Variable | Type | Offsets | +//! |-------------------|---------|-----------------| +//! | `MAX_PROTOCOL_FEE`| uint256 | \[655, 2248\] | +//! +//! ## Storage layout (from `forge inspect ProtocolFee storageLayout`) +//! +//! | Slot | Variable | Type | +//! |------|---------------|---------| +//! | 0 | `_owner` | address | +//! | 1 | `protocolFee` | uint256 | +//! | 2 | `beneficiary` | address | + +use crate::{ + config::ProtocolFeeConfig, + contracts::{ + immutables::{patch_u256, ImmutableRef}, + GenesisContract, + }, +}; +use alloy_primitives::{hex, Bytes, B256, U256}; +use std::collections::BTreeMap; + +/// `ProtocolFee` runtime bytecode compiled with Hyperlane v11.0.3, +/// solc 0.8.22 (Foundry `ci` profile: `cbor_metadata=false`, `bytecode_hash="none"`). +/// +/// Compiled with placeholder immutables (all zeros). Actual values are patched +/// at genesis time via [`build`]. +/// +/// Regenerate with: +/// ```sh +/// cd contracts/lib/hyperlane-monorepo/solidity && \ +/// forge soldeer install && \ +/// FOUNDRY_PROFILE=ci forge inspect ProtocolFee deployedBytecode +/// ``` +const PROTOCOL_FEE_BYTECODE: &[u8] = &hex!("6080604052600436106100dd5760003560e01c8063a1af5b9a1161007f578063b8ca3b8311610059578063b8ca3b831461027d578063e445e7dd146102b1578063e5320bb9146102cd578063f2fde38b146102fd57600080fd5b8063a1af5b9a14610224578063aaccd23014610239578063b0e21e8a1461026757600080fd5b8063715018a6116100bb578063715018a61461016e578063787dce3d146101835780638da5cb5b146101a357806393c44847146101ce57600080fd5b8063086011b9146100e25780631c31f710146100f757806338af3eed14610117575b600080fd5b6100f56100f0366004610e0a565b61031d565b005b34801561010357600080fd5b506100f5610112366004610e76565b6103ca565b34801561012357600080fd5b506002546101449073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561017a57600080fd5b506100f56103de565b34801561018f57600080fd5b506100f561019e366004610eac565b6103f2565b3480156101af57600080fd5b5060005473ffffffffffffffffffffffffffffffffffffffff16610144565b3480156101da57600080fd5b506102176040518060400160405280600681526020017f31312e302e33000000000000000000000000000000000000000000000000000081525081565b6040516101659190610ec5565b34801561023057600080fd5b506100f5610403565b34801561024557600080fd5b50610259610254366004610e0a565b610426565b604051908152602001610165565b34801561027357600080fd5b5061025960015481565b34801561028957600080fd5b506102597f000000000000000000000000000000000000000000000000000000000000000081565b3480156102bd57600080fd5b5060405160088152602001610165565b3480156102d957600080fd5b506102ed6102e8366004610f32565b6104ca565b6040519015158152602001610165565b34801561030957600080fd5b506100f5610318366004610e76565b61051e565b61032784846104ca565b6103b8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603260248201527f4162737472616374506f73744469737061746368486f6f6b3a20696e76616c6960448201527f64206d657461646174612076617269616e74000000000000000000000000000060648201526084015b60405180910390fd5b6103c4848484846105d2565b50505050565b6103d26106d9565b6103db8161075a565b50565b6103e66106d9565b6103f06000610851565b565b6103fa6106d9565b6103db816108c6565b6002546103f09073ffffffffffffffffffffffffffffffffffffffff16476109ab565b600061043285856104ca565b6104be576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603260248201527f4162737472616374506f73744469737061746368486f6f6b3a20696e76616c6960448201527f64206d657461646174612076617269616e74000000000000000000000000000060648201526084016103af565b60015495945050505050565b6000806001541180156104fe575060006104e5848483610b0a565b73ffffffffffffffffffffffffffffffffffffffff1614155b1561050b57506000610518565b6105158383610b5e565b90505b92915050565b6105266106d9565b73ffffffffffffffffffffffffffffffffffffffff81166105c9576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016103af565b6103db81610851565b600154341015610664576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f50726f746f636f6c4665653a20696e73756666696369656e742070726f746f6360448201527f6f6c20666565000000000000000000000000000000000000000000000000000060648201526084016103af565b61066e8282610b80565b73ffffffffffffffffffffffffffffffffffffffff167fb87e607f6030a23ed9b7dac1a717610f3a3b07325269f18808ba763bdcefe7ae6001546040516106b791815260200190565b60405180910390a26103c484848484600154346106d49190610fa3565b610b94565b60005473ffffffffffffffffffffffffffffffffffffffff1633146103f0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016103af565b73ffffffffffffffffffffffffffffffffffffffff81166107d7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f50726f746f636f6c4665653a20696e76616c69642062656e656669636961727960448201526064016103af565b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83169081179091556040519081527f04d55a8be181fb8d75b76f2d48aa0b2ee40f47e53d6e61763eeeec46feea8a24906020015b60405180910390a150565b6000805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b7f0000000000000000000000000000000000000000000000000000000000000000811115610976576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f746f636f6c4665653a2065786365656473206d61782070726f746f636f60448201527f6c2066656500000000000000000000000000000000000000000000000000000060648201526084016103af565b60018190556040518181527fdb5aafdb29539329e37d4e3ee869bc4031941fd55a5dfc92824fbe34b204e30d90602001610846565b80471015610a15576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a20696e73756666696369656e742062616c616e636500000060448201526064016103af565b60008273ffffffffffffffffffffffffffffffffffffffff168260405160006040518083038185875af1925050503d8060008114610a6f576040519150601f19603f3d011682016040523d82523d6000602084013e610a74565b606091505b5050905080610b05576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603a60248201527f416464726573733a20756e61626c6520746f2073656e642076616c75652c207260448201527f6563697069656e74206d6179206861766520726576657274656400000000000060648201526084016103af565b505050565b6000610b1860566014610fb6565b60ff16831015610b29575080610b57565b83605684610b38826014610fb6565b60ff1692610b4893929190610fcf565b610b5191610ff9565b60601c90505b9392505050565b600081158061051557506001610b748484610c80565b61ffff16149392505050565b6000610515610b8f8484610cd1565b610cea565b8015610c79576000610bb2610ba98585610b80565b87908790610d93565b905073ffffffffffffffffffffffffffffffffffffffff8116610c57576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f4162737472616374506f73744469737061746368486f6f6b3a206e6f2072656660448201527f756e64206164647265737300000000000000000000000000000000000000000060648201526084016103af565b610c7773ffffffffffffffffffffffffffffffffffffffff8216836109ab565b505b5050505050565b6000610c8d816002610fb6565b60ff16821015610c9f57506000610518565b82600083610cae826002610fb6565b60ff1692610cbe93929190610fcf565b610cc791611041565b60f01c9392505050565b6000610ce1602960098486610fcf565b61051591611087565b600073ffffffffffffffffffffffffffffffffffffffff821115610d8f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f5479706543617374733a2062797465733332546f41646472657373206f76657260448201527f666c6f770000000000000000000000000000000000000000000000000000000060648201526084016103af565b5090565b6000610da160426014610fb6565b60ff16831015610db2575080610b57565b83604284610b38826014610fb6565b60008083601f840112610dd357600080fd5b50813567ffffffffffffffff811115610deb57600080fd5b602083019150836020828501011115610e0357600080fd5b9250929050565b60008060008060408587031215610e2057600080fd5b843567ffffffffffffffff80821115610e3857600080fd5b610e4488838901610dc1565b90965094506020870135915080821115610e5d57600080fd5b50610e6a87828801610dc1565b95989497509550505050565b600060208284031215610e8857600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610b5757600080fd5b600060208284031215610ebe57600080fd5b5035919050565b60006020808352835180602085015260005b81811015610ef357858101830151858201604001528201610ed7565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b60008060208385031215610f4557600080fd5b823567ffffffffffffffff811115610f5c57600080fd5b610f6885828601610dc1565b90969095509350505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8181038181111561051857610518610f74565b60ff818116838216019081111561051857610518610f74565b60008085851115610fdf57600080fd5b83861115610fec57600080fd5b5050820193919092039150565b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000081358181169160148510156110395780818660140360031b1b83161692505b505092915050565b7fffff00000000000000000000000000000000000000000000000000000000000081358181169160028510156110395760029490940360031b84901b1690921692915050565b80356020831015610518577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff602084900360031b1b169291505056"); + +// ── Immutable reference offsets (from `forge inspect ProtocolFee immutableReferences`) ── + +/// `MAX_PROTOCOL_FEE` (uint256) — maximum fee that can be set. +const MAX_PROTOCOL_FEE_REFS: &[ImmutableRef] = &[ + ImmutableRef { + start: 655, + length: 32, + }, + ImmutableRef { + start: 2248, + length: 32, + }, +]; + +/// Build a genesis alloc entry for `ProtocolFee`. +pub(crate) fn build(config: &ProtocolFeeConfig) -> GenesisContract { + let mut bytecode = PROTOCOL_FEE_BYTECODE.to_vec(); + + // Patch immutables + patch_u256( + &mut bytecode, + MAX_PROTOCOL_FEE_REFS, + U256::from(config.max_protocol_fee), + ); + + let mut storage = BTreeMap::new(); + + // Slot 0: _owner + if !config.owner.is_zero() { + storage.insert( + B256::ZERO, + B256::from(U256::from_be_bytes(config.owner.into_word().0)), + ); + } + + // Slot 1: protocolFee + if config.protocol_fee > 0 { + storage.insert( + B256::from(U256::from(1u64)), + B256::from(U256::from(config.protocol_fee)), + ); + } + + // Slot 2: beneficiary + if !config.beneficiary.is_zero() { + storage.insert( + B256::from(U256::from(2u64)), + B256::from(U256::from_be_bytes(config.beneficiary.into_word().0)), + ); + } + + GenesisContract { + address: config.address, + code: Bytes::from(bytecode), + storage, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, hex}; + use std::{path::PathBuf, process::Command}; + + fn test_config() -> ProtocolFeeConfig { + ProtocolFeeConfig { + address: address!("0000000000000000000000000000000000001300"), + owner: address!("000000000000000000000000000000000000ad00"), + max_protocol_fee: 1_000_000_000_000_000_000, + protocol_fee: 100_000, + beneficiary: address!("000000000000000000000000000000000000be00"), + } + } + + #[test] + fn storage_has_owner() { + let contract = build(&test_config()); + let expected: B256 = "0x000000000000000000000000000000000000000000000000000000000000Ad00" + .parse() + .unwrap(); + assert_eq!(contract.storage[&B256::ZERO], expected); + } + + #[test] + fn bytecode_is_patched_with_max_protocol_fee() { + let config = test_config(); + let contract = build(&config); + let code = contract.code.to_vec(); + + let expected = B256::from(U256::from(config.max_protocol_fee)); + for &offset in &[655, 2248] { + let word = &code[offset..offset + 32]; + assert_eq!( + word, + expected.as_slice(), + "max_protocol_fee not patched at offset {offset}" + ); + } + } + + #[test] + fn zero_protocol_fee_omits_slot_1() { + let config = ProtocolFeeConfig { + address: address!("0000000000000000000000000000000000001300"), + owner: address!("000000000000000000000000000000000000ad00"), + max_protocol_fee: 1_000_000_000_000_000_000, + protocol_fee: 0, + beneficiary: address!("000000000000000000000000000000000000be00"), + }; + let contract = build(&config); + let fee_slot = B256::from(U256::from(1u64)); + assert!( + !contract.storage.contains_key(&fee_slot), + "zero protocol_fee should not produce a storage entry" + ); + } + + #[test] + fn storage_count_for_standard_config() { + let contract = build(&test_config()); + // Should have exactly 3 storage entries: _owner (slot 0), protocolFee (slot 1), beneficiary (slot 2) + assert_eq!(contract.storage.len(), 3); + } + + #[test] + #[ignore = "requires forge CLI"] + fn protocol_fee_bytecode_matches_solidity_source() { + let contracts_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .unwrap() + .join("contracts") + .join("lib") + .join("hyperlane-monorepo") + .join("solidity"); + + let output = Command::new("forge") + .args(["inspect", "ProtocolFee", "deployedBytecode", "--root"]) + .arg(&contracts_root) + .env("FOUNDRY_PROFILE", "ci") + .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() + .strip_prefix("0x") + .unwrap() + .to_lowercase(); + + let hardcoded_hex = hex::encode(PROTOCOL_FEE_BYTECODE); + + assert_eq!( + forge_hex, hardcoded_hex, + "ProtocolFee bytecode mismatch! Regenerate with: \ + cd contracts/lib/hyperlane-monorepo/solidity && \ + FOUNDRY_PROFILE=ci forge inspect ProtocolFee deployedBytecode" + ); + } +} diff --git a/bin/ev-deployer/src/genesis.rs b/bin/ev-deployer/src/genesis.rs index 9b200769..eb3b7283 100644 --- a/bin/ev-deployer/src/genesis.rs +++ b/bin/ev-deployer/src/genesis.rs @@ -22,6 +22,28 @@ pub(crate) fn build_alloc(config: &DeployConfig) -> Value { insert_contract(&mut alloc, &contract); } + if let Some(ref mth_config) = config.contracts.merkle_tree_hook { + let local_domain = config.chain.chain_id as u32; + let contract = contracts::merkle_tree_hook::build(mth_config, local_domain); + insert_contract(&mut alloc, &contract); + } + + if let Some(ref mb_config) = config.contracts.mailbox { + let local_domain = config.chain.chain_id as u32; + let contract = contracts::mailbox::build(mb_config, local_domain); + insert_contract(&mut alloc, &contract); + } + + if let Some(ref ni_config) = config.contracts.noop_ism { + let contract = contracts::noop_ism::build(ni_config); + insert_contract(&mut alloc, &contract); + } + + if let Some(ref pf_config) = config.contracts.protocol_fee { + let contract = contracts::protocol_fee::build(pf_config); + insert_contract(&mut alloc, &contract); + } + Value::Object(alloc) } @@ -98,6 +120,10 @@ mod tests { owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), }), fee_vault: None, + merkle_tree_hook: None, + mailbox: None, + noop_ism: None, + protocol_fee: None, }, } } diff --git a/bin/ev-deployer/src/main.rs b/bin/ev-deployer/src/main.rs index 42ad6a4a..2fb6f901 100644 --- a/bin/ev-deployer/src/main.rs +++ b/bin/ev-deployer/src/main.rs @@ -109,6 +109,30 @@ fn main() -> eyre::Result<()> { .as_ref() .map(|c| c.address) .ok_or_else(|| eyre::eyre!("fee_vault not configured"))?, + "merkle_tree_hook" => cfg + .contracts + .merkle_tree_hook + .as_ref() + .map(|c| c.address) + .ok_or_else(|| eyre::eyre!("merkle_tree_hook not configured"))?, + "mailbox" => cfg + .contracts + .mailbox + .as_ref() + .map(|c| c.address) + .ok_or_else(|| eyre::eyre!("mailbox not configured"))?, + "noop_ism" => cfg + .contracts + .noop_ism + .as_ref() + .map(|c| c.address) + .ok_or_else(|| eyre::eyre!("noop_ism not configured"))?, + "protocol_fee" => cfg + .contracts + .protocol_fee + .as_ref() + .map(|c| c.address) + .ok_or_else(|| eyre::eyre!("protocol_fee not configured"))?, other => eyre::bail!("unknown contract: {other}"), }; diff --git a/bin/ev-deployer/src/output.rs b/bin/ev-deployer/src/output.rs index 22bf063c..56e62608 100644 --- a/bin/ev-deployer/src/output.rs +++ b/bin/ev-deployer/src/output.rs @@ -21,5 +21,33 @@ pub(crate) fn build_manifest(config: &DeployConfig) -> Value { ); } + if let Some(ref mth) = config.contracts.merkle_tree_hook { + manifest.insert( + "merkle_tree_hook".to_string(), + Value::String(format!("{}", mth.address)), + ); + } + + if let Some(ref mb) = config.contracts.mailbox { + manifest.insert( + "mailbox".to_string(), + Value::String(format!("{}", mb.address)), + ); + } + + if let Some(ref ni) = config.contracts.noop_ism { + manifest.insert( + "noop_ism".to_string(), + Value::String(format!("{}", ni.address)), + ); + } + + if let Some(ref pf) = config.contracts.protocol_fee { + manifest.insert( + "protocol_fee".to_string(), + Value::String(format!("{}", pf.address)), + ); + } + Value::Object(manifest) } diff --git a/bin/ev-deployer/tests/e2e_genesis.sh b/bin/ev-deployer/tests/e2e_genesis.sh index 3b7783b4..2a8b9a6d 100755 --- a/bin/ev-deployer/tests/e2e_genesis.sh +++ b/bin/ev-deployer/tests/e2e_genesis.sh @@ -81,8 +81,10 @@ grep -q "000000000000000000000000000000000000Ad00" "$GENESIS" \ || fail "AdminProxy address not found in genesis" grep -q "000000000000000000000000000000000000FE00" "$GENESIS" \ || fail "FeeVault address not found in genesis" +grep -q "0000000000000000000000000000000000001100" "$GENESIS" \ + || fail "MerkleTreeHook address not found in genesis" -pass "genesis contains both contract addresses" +pass "genesis contains all three contract addresses" # ── Step 3: Start ev-reth ──────────────────────────────── @@ -162,6 +164,81 @@ expected_slot6="0x00000000000000000000000000000000000000000000000000000000000027 || fail "FeeVault slot 6 (bridgeShareBps) mismatch: got $fv_slot6, expected $expected_slot6" pass "FeeVault slot 6 (bridgeShareBps) = 10000" +# ── Step 6: Verify MerkleTreeHook ──────────────────────── + +MERKLE_TREE_HOOK="0x0000000000000000000000000000000000001100" +MERKLE_TREE_HOOK_OWNER="0x000000000000000000000000000000000000Ad00" +MERKLE_TREE_HOOK_MAILBOX="0x0000000000000000000000000000000000001200" + +echo "=== Verifying MerkleTreeHook at $MERKLE_TREE_HOOK ===" + +# Check code is present +mth_code=$(rpc_call "eth_getCode" "[\"$MERKLE_TREE_HOOK\", \"latest\"]") +[[ "$mth_code" != "0x" && "$mth_code" != "0x0" && ${#mth_code} -gt 10 ]] \ + || fail "MerkleTreeHook has no bytecode (got: $mth_code)" +pass "MerkleTreeHook has bytecode (${#mth_code} hex chars)" + +# Compare full bytecode against genesis JSON +# Extract expected code from genesis for the MerkleTreeHook address +expected_mth_code=$(python3 -c " +import json, sys +with open('$GENESIS') as f: + genesis = json.load(f) +alloc = genesis['alloc'] +# Address key is checksummed without 0x prefix +entry = alloc.get('0000000000000000000000000000000000001100') +print(entry['code']) +") +[[ "$(echo "$mth_code" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_mth_code" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "MerkleTreeHook bytecode from node does not match genesis JSON" +pass "MerkleTreeHook bytecode matches genesis JSON" + +# Slot 0: _initialized = 1 (OZ v4 Initializable) +mth_slot0=$(rpc_call "eth_getStorageAt" "[\"$MERKLE_TREE_HOOK\", \"0x0\", \"latest\"]") +expected_init="0x0000000000000000000000000000000000000000000000000000000000000001" +[[ "$(echo "$mth_slot0" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_init" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "MerkleTreeHook slot 0 (_initialized) mismatch: got $mth_slot0, expected $expected_init" +pass "MerkleTreeHook slot 0 (_initialized) = 1" + +# Slot 51 (0x33): _owner +mth_slot51=$(rpc_call "eth_getStorageAt" "[\"$MERKLE_TREE_HOOK\", \"0x33\", \"latest\"]") +expected_owner="0x000000000000000000000000000000000000000000000000000000000000ad00" +[[ "$(echo "$mth_slot51" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_owner" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "MerkleTreeHook slot 51 (_owner) mismatch: got $mth_slot51, expected $expected_owner" +pass "MerkleTreeHook slot 51 (_owner) = $MERKLE_TREE_HOOK_OWNER" + +# Verify immutables are patched in bytecode: +# mailbox address at byte offsets 904 and 3300 (each is a 32-byte word, address in last 20 bytes) +# localDomain (chain_id=1234=0x04d2) at byte offset 644 +# The hex string has "0x" prefix, so byte N in the bytecode = hex chars at positions 2+2N..2+2N+2 +mth_hex="${mth_code#0x}" + +check_immutable() { + local name="$1" + local byte_offset="$2" + local expected_hex="$3" + local hex_offset=$((byte_offset * 2)) + local hex_len=${#expected_hex} + local actual="${mth_hex:$hex_offset:$hex_len}" + [[ "$(echo "$actual" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_hex" | tr '[:upper:]' '[:lower:]')" ]] \ + || fail "$name at byte offset $byte_offset mismatch: got $actual, expected $expected_hex" + pass "$name patched correctly at byte offset $byte_offset" +} + +# mailbox = 0x...1200 → 32-byte word with address in last 20 bytes +# Full 32-byte word: 000000000000000000000000 + 0000000000000000000000000000000000001200 +mailbox_word="0000000000000000000000000000000000000000000000000000000000001200" +check_immutable "mailbox" 904 "$mailbox_word" +check_immutable "mailbox (second ref)" 3300 "$mailbox_word" + +# localDomain = chain_id 1234 = 0x04d2 → 32-byte word +domain_word="00000000000000000000000000000000000000000000000000000000000004d2" +check_immutable "localDomain" 644 "$domain_word" + +# deployedBlock = 0 → 32 zero bytes +deployed_block_word="0000000000000000000000000000000000000000000000000000000000000000" +check_immutable "deployedBlock" 578 "$deployed_block_word" + # ── Done ───────────────────────────────────────────────── echo "" diff --git a/contracts/lib/hyperlane-monorepo b/contracts/lib/hyperlane-monorepo new file mode 160000 index 00000000..bc401f7a --- /dev/null +++ b/contracts/lib/hyperlane-monorepo @@ -0,0 +1 @@ +Subproject commit bc401f7a64f9e43aa25265dba12d80a33a19de21