diff --git a/Cargo.lock b/Cargo.lock index 3d0e154..318aaf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2895,6 +2895,7 @@ dependencies = [ "hex", "reth-basic-payload-builder", "reth-chainspec", + "reth-cli", "reth-consensus", "reth-db", "reth-engine-local", diff --git a/README.md b/README.md index 583be4b..04d0b27 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,31 @@ Implementation details: - No runtime environment variables are required; the chainspec carries the policy alongside other fork settings - When not configured, the EVM operates normally with standard fee burning +### Custom EIP-1559 Parameters (Custom Networks Only) + +ev-reth also lets you override EIP-1559 base fee parameters through the same `evolve` stanza in +your chainspec. This is consensus-critical: all nodes must use the same values. + +```json +"config": { + ..., + "evolve": { + "baseFeeMaxChangeDenominator": 8, + "baseFeeElasticityMultiplier": 2, + "initialBaseFeePerGas": 1000000000 + } +} +``` + +Notes: + +- `baseFeeMaxChangeDenominator` and `baseFeeElasticityMultiplier` override the EIP-1559 formula. +- `initialBaseFeePerGas` only applies when `londonBlock` is `0` (London at genesis). It updates the + genesis `baseFeePerGas` value; if London is activated later, the initial base fee remains + hardcoded to the EIP-1559 constant. +- The node will fail fast if these values are invalid or inconsistent. +- See `docs/eip1559-configuration.md` for recommended values at 100ms block times. + ### Custom Contract Size Limit By default, Ethereum enforces a 24KB contract size limit per [EIP-170](https://eips.ethereum.org/EIPS/eip-170). If your network requires larger contracts, `ev-reth` supports configuring a custom limit via the chainspec. diff --git a/bin/ev-reth/src/main.rs b/bin/ev-reth/src/main.rs index cab2623..e2d9ac9 100644 --- a/bin/ev-reth/src/main.rs +++ b/bin/ev-reth/src/main.rs @@ -10,12 +10,12 @@ use evolve_ev_reth::{ config::EvolveConfig, rpc::txpool::{EvolveTxpoolApiImpl, EvolveTxpoolApiServer}, }; -use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, Cli}; +use reth_ethereum_cli::Cli; use reth_tracing_otlp::layer as otlp_layer; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; -use ev_node::{log_startup, EvolveArgs, EvolveNode}; +use ev_node::{log_startup, EvolveArgs, EvolveChainSpecParser, EvolveNode}; #[global_allocator] static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator(); @@ -47,8 +47,8 @@ fn main() { init_otlp_tracing(); } - if let Err(err) = Cli::::parse().run( - |builder, _evolve_args| async move { + if let Err(err) = + Cli::::parse().run(|builder, _evolve_args| async move { log_startup(); let handle = builder .node(EvolveNode::new()) @@ -67,8 +67,8 @@ fn main() { info!("=== EV-RETH: Node launched successfully with ev-reth payload builder ==="); handle.node_exit_future.await - }, - ) { + }) + { eprintln!("Error: {err:?}"); std::process::exit(1); } diff --git a/contracts/script/GenerateAdminProxyAlloc.s.sol b/contracts/script/GenerateAdminProxyAlloc.s.sol index 423a209..9f92ed9 100644 --- a/contracts/script/GenerateAdminProxyAlloc.s.sol +++ b/contracts/script/GenerateAdminProxyAlloc.s.sol @@ -52,9 +52,9 @@ contract GenerateAdminProxyAlloc is Script { console.log(' "alloc": {'); console.log(' "000000000000000000000000000000000000Ad00": {'); console.log(' "balance": "0x0",'); - console.log(' "code": "0x%s",', vm.toString(runtimeCode)); + console.log(' "code": "%s",', vm.toString(runtimeCode)); console.log(' "storage": {'); - console.log(' "0x0": "0x%s"', vm.toString(ownerSlotValue)); + console.log(' "0x0": "%s"', vm.toString(ownerSlotValue)); console.log(" }"); console.log(" }"); console.log(" }"); @@ -100,9 +100,9 @@ contract GenerateAdminProxyAllocJSON is Script { // Output minimal JSON that can be merged into genesis string memory json = string( abi.encodePacked( - '{"000000000000000000000000000000000000Ad00":{"balance":"0x0","code":"0x', + '{"000000000000000000000000000000000000Ad00":{"balance":"0x0","code":"', vm.toString(runtimeCode), - '","storage":{"0x0":"0x', + '","storage":{"0x0":"', vm.toString(ownerSlotValue), '"}}}' ) diff --git a/contracts/script/GenerateFeeVaultAlloc.s.sol b/contracts/script/GenerateFeeVaultAlloc.s.sol new file mode 100644 index 0000000..0215452 --- /dev/null +++ b/contracts/script/GenerateFeeVaultAlloc.s.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Script, console} from "forge-std/Script.sol"; +import {FeeVault} from "../src/FeeVault.sol"; + +abstract contract FeeVaultAllocBase is Script { + struct Config { + address feeVaultAddress; + address owner; + uint32 destinationDomain; + bytes32 recipientAddress; + uint256 minimumAmount; + uint256 callFee; + uint256 bridgeShareBpsRaw; + uint256 bridgeShareBps; + address otherRecipient; + address hypNativeMinter; + bytes32 salt; + address deployer; + } + + function loadConfig() internal view returns (Config memory cfg) { + cfg.owner = vm.envAddress("OWNER"); + cfg.destinationDomain = uint32(vm.envOr("DESTINATION_DOMAIN", uint256(0))); + cfg.recipientAddress = vm.envOr("RECIPIENT_ADDRESS", bytes32(0)); + cfg.minimumAmount = vm.envOr("MINIMUM_AMOUNT", uint256(0)); + cfg.callFee = vm.envOr("CALL_FEE", uint256(0)); + cfg.bridgeShareBpsRaw = vm.envOr("BRIDGE_SHARE_BPS", uint256(0)); + cfg.otherRecipient = vm.envOr("OTHER_RECIPIENT", address(0)); + cfg.hypNativeMinter = vm.envOr("HYP_NATIVE_MINTER", address(0)); + cfg.feeVaultAddress = vm.envOr("FEE_VAULT_ADDRESS", address(0)); + cfg.deployer = vm.envOr("DEPLOYER", address(0)); + cfg.salt = vm.envOr("SALT", bytes32(0)); + + require(cfg.owner != address(0), "OWNER required"); + require(cfg.bridgeShareBpsRaw <= 10000, "BRIDGE_SHARE_BPS > 10000"); + + cfg.bridgeShareBps = cfg.bridgeShareBpsRaw == 0 ? 10000 : cfg.bridgeShareBpsRaw; + + if (cfg.feeVaultAddress == address(0) && cfg.deployer != address(0)) { + bytes32 initCodeHash = keccak256( + abi.encodePacked( + type(FeeVault).creationCode, + abi.encode( + cfg.owner, + cfg.destinationDomain, + cfg.recipientAddress, + cfg.minimumAmount, + cfg.callFee, + cfg.bridgeShareBpsRaw, + cfg.otherRecipient + ) + ) + ); + cfg.feeVaultAddress = address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), cfg.deployer, cfg.salt, initCodeHash)))) + ); + } + + require(cfg.feeVaultAddress != address(0), "FEE_VAULT_ADDRESS or DEPLOYER required"); + } + + function computeSlots(Config memory cfg) + internal + pure + returns ( + bytes32 slot0, + bytes32 slot1, + bytes32 slot2, + bytes32 slot3, + bytes32 slot4, + bytes32 slot5, + bytes32 slot6 + ) + { + slot0 = bytes32(uint256(uint160(cfg.hypNativeMinter))); + slot1 = bytes32((uint256(cfg.destinationDomain) << 160) | uint256(uint160(cfg.owner))); + slot2 = cfg.recipientAddress; + slot3 = bytes32(cfg.minimumAmount); + slot4 = bytes32(cfg.callFee); + slot5 = bytes32(uint256(uint160(cfg.otherRecipient))); + slot6 = bytes32(cfg.bridgeShareBps); + } + + function addressKey(address addr) internal pure returns (string memory) { + bytes memory full = bytes(vm.toString(addr)); + bytes memory key = new bytes(40); + // Fixed-length copy for address key without 0x prefix. + for (uint256 i = 0; i < 40; i++) { + key[i] = full[i + 2]; + } + return string(key); + } +} + +/// @title GenerateFeeVaultAlloc +/// @notice Generates genesis alloc JSON for deploying FeeVault at a deterministic address +/// @dev Run with: OWNER=0x... forge script script/GenerateFeeVaultAlloc.s.sol -vvv +contract GenerateFeeVaultAlloc is FeeVaultAllocBase { + function run() external view { + Config memory cfg = loadConfig(); + bytes memory runtimeCode = type(FeeVault).runtimeCode; + + (bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5, bytes32 slot6) = + computeSlots(cfg); + + console.log("========== FeeVault Genesis Alloc =========="); + console.log("FeeVault address:", cfg.feeVaultAddress); + console.log("Owner:", cfg.owner); + console.log("Destination domain:", cfg.destinationDomain); + console.log("Bridge share bps (raw):", cfg.bridgeShareBpsRaw); + console.log("Bridge share bps (effective):", cfg.bridgeShareBps); + console.log(""); + + if (cfg.bridgeShareBpsRaw == 0) { + console.log("NOTE: BRIDGE_SHARE_BPS=0 defaults to 10000 (constructor behavior)."); + } + if (cfg.bridgeShareBps < 10000 && cfg.otherRecipient == address(0)) { + console.log("WARNING: OTHER_RECIPIENT is zero but bridge share < 10000."); + } + if (cfg.hypNativeMinter == address(0)) { + console.log("NOTE: HYP_NATIVE_MINTER is zero; set it before calling sendToCelestia()."); + } + console.log(""); + + console.log("Add this to your genesis.json 'alloc' section:"); + console.log(""); + console.log("{"); + console.log(' "alloc": {'); + console.log(' "%s": {', addressKey(cfg.feeVaultAddress)); + console.log(' "balance": "0x0",'); + console.log(' "code": "%s",', vm.toString(runtimeCode)); + console.log(' "storage": {'); + console.log(' "0x0": "%s",', vm.toString(slot0)); + console.log(' "0x1": "%s",', vm.toString(slot1)); + console.log(' "0x2": "%s",', vm.toString(slot2)); + console.log(' "0x3": "%s",', vm.toString(slot3)); + console.log(' "0x4": "%s",', vm.toString(slot4)); + console.log(' "0x5": "%s",', vm.toString(slot5)); + console.log(' "0x6": "%s"', vm.toString(slot6)); + console.log(" }"); + console.log(" }"); + console.log(" }"); + console.log("}"); + console.log(""); + console.log("Raw bytecode length:", runtimeCode.length); + console.log("============================================="); + } +} + +/// @title GenerateFeeVaultAllocJSON +/// @notice Outputs just the JSON snippet for easy copy-paste +/// @dev Run with: OWNER=0x... forge script script/GenerateFeeVaultAlloc.s.sol:GenerateFeeVaultAllocJSON -vvv +contract GenerateFeeVaultAllocJSON is FeeVaultAllocBase { + function run() external view { + Config memory cfg = loadConfig(); + bytes memory runtimeCode = type(FeeVault).runtimeCode; + + (bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5, bytes32 slot6) = + computeSlots(cfg); + + string memory json = string( + abi.encodePacked( + '{"', + addressKey(cfg.feeVaultAddress), + '":{"balance":"0x0","code":"', + vm.toString(runtimeCode), + '","storage":{"0x0":"', + vm.toString(slot0), + '","0x1":"', + vm.toString(slot1), + '","0x2":"', + vm.toString(slot2), + '","0x3":"', + vm.toString(slot3), + '","0x4":"', + vm.toString(slot4), + '","0x5":"', + vm.toString(slot5), + '","0x6":"', + vm.toString(slot6), + '"}}}' + ) + ); + + console.log(json); + } +} diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index f2d7813..635ec38 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -17,6 +17,7 @@ ev-revm = { path = "../ev-revm" } # Reth dependencies reth-node-builder.workspace = true reth-chainspec.workspace = true +reth-cli.workspace = true reth-ethereum = { workspace = true, features = ["node", "cli", "pool"] } reth-ethereum-forks.workspace = true reth-ethereum-payload-builder.workspace = true @@ -52,6 +53,7 @@ alloy-primitives.workspace = true alloy-eips.workspace = true alloy-consensus.workspace = true alloy-evm.workspace = true +alloy-genesis.workspace = true # Core dependencies eyre.workspace = true @@ -73,7 +75,6 @@ reth-transaction-pool.workspace = true reth-consensus.workspace = true reth-tasks.workspace = true reth-tracing.workspace = true -alloy-genesis.workspace = true tempfile.workspace = true hex = "0.4" diff --git a/crates/node/src/chainspec.rs b/crates/node/src/chainspec.rs new file mode 100644 index 0000000..1c7a2c8 --- /dev/null +++ b/crates/node/src/chainspec.rs @@ -0,0 +1,218 @@ +use alloy_genesis::Genesis; +use eyre::{bail, eyre, Result, WrapErr}; +use reth_chainspec::{BaseFeeParamsKind, ChainSpec, DEV, HOLESKY, HOODI, MAINNET, SEPOLIA}; +use reth_cli::chainspec::{parse_genesis, ChainSpecParser}; +use serde::Deserialize; +use std::sync::Arc; + +/// Chains supported by ev-reth. First value should be used as the default. +pub const SUPPORTED_CHAINS: &[&str] = &["mainnet", "sepolia", "holesky", "hoodi", "dev"]; + +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct EvolveEip1559Config { + base_fee_max_change_denominator: Option, + base_fee_elasticity_multiplier: Option, + initial_base_fee_per_gas: Option, +} + +impl EvolveEip1559Config { + const fn has_base_fee_overrides(&self) -> bool { + self.base_fee_max_change_denominator.is_some() + || self.base_fee_elasticity_multiplier.is_some() + } +} + +/// Chainspec parser that applies ev-reth specific EIP-1559 overrides from the evolve extras block. +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct EvolveChainSpecParser; + +impl ChainSpecParser for EvolveChainSpecParser { + type ChainSpec = ChainSpec; + + const SUPPORTED_CHAINS: &'static [&'static str] = SUPPORTED_CHAINS; + + fn parse(s: &str) -> Result> { + match s { + "mainnet" => Ok(MAINNET.clone()), + "sepolia" => Ok(SEPOLIA.clone()), + "holesky" => Ok(HOLESKY.clone()), + "hoodi" => Ok(HOODI.clone()), + "dev" => Ok(DEV.clone()), + _ => parse_custom_chain_spec(s), + } + } +} + +fn parse_custom_chain_spec(input: &str) -> Result> { + let mut genesis = parse_genesis(input).wrap_err("Failed to parse genesis config")?; + let overrides = parse_eip1559_overrides(&genesis)?; + apply_genesis_overrides(&mut genesis, &overrides)?; + + let mut chain_spec: ChainSpec = genesis.into(); + apply_chain_spec_overrides(&mut chain_spec, &overrides)?; + + Ok(Arc::new(chain_spec)) +} + +fn parse_eip1559_overrides(genesis: &Genesis) -> Result { + match genesis + .config + .extra_fields + .get_deserialized::("evolve") + { + Some(Ok(config)) => Ok(config), + Some(Err(err)) => Err(eyre!(err)).wrap_err("Invalid evolve extras in chainspec"), + None => Ok(EvolveEip1559Config::default()), + } +} + +fn apply_genesis_overrides(genesis: &mut Genesis, overrides: &EvolveEip1559Config) -> Result<()> { + let Some(initial_base_fee) = overrides.initial_base_fee_per_gas else { + return Ok(()); + }; + + if genesis.config.london_block != Some(0) { + bail!("initialBaseFeePerGas requires londonBlock set to 0 in the chainspec config"); + } + + let initial_base_fee_u128 = u128::from(initial_base_fee); + if let Some(existing) = genesis.base_fee_per_gas { + if existing != initial_base_fee_u128 { + bail!( + "initialBaseFeePerGas conflicts with baseFeePerGas in genesis ({} != {})", + initial_base_fee_u128, + existing + ); + } + } + + genesis.base_fee_per_gas = Some(initial_base_fee_u128); + Ok(()) +} + +fn apply_chain_spec_overrides( + chain_spec: &mut ChainSpec, + overrides: &EvolveEip1559Config, +) -> Result<()> { + if let Some(denominator) = overrides.base_fee_max_change_denominator { + if denominator == 0 { + bail!( + "baseFeeMaxChangeDenominator must be greater than 0, got: {}", + denominator + ); + } + } + + if let Some(elasticity) = overrides.base_fee_elasticity_multiplier { + if elasticity == 0 { + bail!( + "baseFeeElasticityMultiplier must be greater than 0, got: {}", + elasticity + ); + } + } + + if !overrides.has_base_fee_overrides() { + return Ok(()); + } + + let mut params = chain_spec.base_fee_params_at_timestamp(chain_spec.genesis.timestamp); + + if let Some(denominator) = overrides.base_fee_max_change_denominator { + params.max_change_denominator = u128::from(denominator); + } + + if let Some(elasticity) = overrides.base_fee_elasticity_multiplier { + params.elasticity_multiplier = u128::from(elasticity); + } + + chain_spec.base_fee_params = BaseFeeParamsKind::Constant(params); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_genesis::Genesis; + use serde_json::json; + + fn apply_overrides(genesis: &Genesis) -> Result { + let overrides = parse_eip1559_overrides(genesis)?; + let mut genesis = genesis.clone(); + apply_genesis_overrides(&mut genesis, &overrides)?; + let mut chain_spec: ChainSpec = genesis.into(); + apply_chain_spec_overrides(&mut chain_spec, &overrides)?; + Ok(chain_spec) + } + + #[test] + fn test_eip1559_overrides_apply() { + let mut genesis = Genesis::default(); + genesis.config.chain_id = 1; + genesis.config.london_block = Some(0); + genesis + .config + .extra_fields + .insert_value( + "evolve".to_string(), + json!({ + "baseFeeMaxChangeDenominator": 10, + "baseFeeElasticityMultiplier": 4, + "initialBaseFeePerGas": 7 + }), + ) + .unwrap(); + + let chain_spec = apply_overrides(&genesis).unwrap(); + let params = chain_spec.base_fee_params_at_timestamp(chain_spec.genesis.timestamp); + assert_eq!(params.max_change_denominator, 10); + assert_eq!(params.elasticity_multiplier, 4); + assert_eq!(chain_spec.genesis.base_fee_per_gas, Some(7)); + } + + #[test] + fn test_initial_base_fee_requires_london_genesis() { + let mut genesis = Genesis::default(); + genesis.config.chain_id = 1; + genesis.config.london_block = Some(10); + genesis + .config + .extra_fields + .insert_value( + "evolve".to_string(), + json!({ + "initialBaseFeePerGas": 7 + }), + ) + .unwrap(); + + let err = apply_overrides(&genesis).unwrap_err(); + assert!(err + .to_string() + .contains("initialBaseFeePerGas requires londonBlock set to 0")); + } + + #[test] + fn test_base_fee_denominator_must_be_positive() { + let mut genesis = Genesis::default(); + genesis.config.chain_id = 1; + genesis.config.london_block = Some(0); + genesis + .config + .extra_fields + .insert_value( + "evolve".to_string(), + json!({ + "baseFeeMaxChangeDenominator": 0 + }), + ) + .unwrap(); + + let err = apply_overrides(&genesis).unwrap_err(); + assert!(err + .to_string() + .contains("baseFeeMaxChangeDenominator must be greater than 0")); + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 8591d28..b25d5d0 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -11,6 +11,8 @@ pub mod args; pub mod attributes; /// Builder module for payload construction and related utilities. pub mod builder; +/// Chainspec parser with ev-reth overrides. +pub mod chainspec; /// Configuration types and validation for the Evolve payload builder. pub mod config; /// Shared error types for evolve node wiring. @@ -28,6 +30,7 @@ pub mod validator; pub use args::EvolveArgs; pub use attributes::{EvolveEnginePayloadAttributes, EvolveEnginePayloadBuilderAttributes}; pub use builder::{create_payload_builder_service, EvolvePayloadBuilder}; +pub use chainspec::EvolveChainSpecParser; pub use config::{ConfigError, EvolvePayloadBuilderConfig}; pub use error::EvolveEngineError; pub use executor::{build_evm_config, EvolveEvmConfig, EvolveExecutorBuilder}; diff --git a/docs/contracts/fee_vault.md b/docs/contracts/fee_vault.md index f7a4d77..ed4ffe5 100644 --- a/docs/contracts/fee_vault.md +++ b/docs/contracts/fee_vault.md @@ -1,9 +1,11 @@ # FeeVault Design & Use Case ## Overview + The `FeeVault` is a specialized smart contract designed to accumulate native tokens (gas tokens) and automatically split them between bridging to a specific destination chain (e.g., Celestia) and sending to a secondary recipient. ## Use Case + This contract serves as a **fee sink** and **bridging mechanism** for a rollup or chain that wants to redirect collected fees (e.g., EIP-1559 base fees) to another ecosystem while retaining a portion for other purposes (e.g., developer rewards, treasury). 1. **Fee Accumulation**: The contract receives funds from: @@ -17,10 +19,12 @@ This contract serves as a **fee sink** and **bridging mechanism** for a rollup o ## Architecture ### Core Components + - **HypNativeMinter Integration**: The contract interacts with a Hyperlane `HypNativeMinter` to handle the cross-chain transfer logic. - **Admin Controls**: An `owner` manages critical parameters to ensure security and flexibility. ### Key Features + - **Automatic Splitting**: Funds are split automatically upon calling `sendToCelestia`. No manual withdrawal is required for the secondary recipient. - **Stored Recipient**: The destination domain (Chain ID) and recipient address are stored in the contract state. - **Minimum Threshold**: A `minimumAmount` ensures that bridging only occurs when it is economically viable. @@ -46,6 +50,7 @@ This contract serves as a **fee sink** and **bridging mechanism** for a rollup o - `SentToCelestia` and `FundsSplit` events are emitted. ## Configuration Parameters + | Parameter | Description | Managed By | |-----------|-------------|------------| | `destinationDomain` | Hyperlane domain ID of the target chain (e.g., Celestia). | Owner | @@ -54,3 +59,124 @@ This contract serves as a **fee sink** and **bridging mechanism** for a rollup o | `callFee` | Fee required from the caller to execute the function. | Owner | | `bridgeShareBps` | Basis points (0-10000) determining the % of funds to bridge. | Owner | | `otherRecipient` | Address to receive the non-bridged portion of funds. | Owner | + +## Embedding FeeVault in Genesis + +Embedding FeeVault in genesis means pre-deploying the runtime bytecode and setting storage slots directly. The constructor does **not** run, so every needed value must be written into `alloc.storage`. + +### 1. Choose the FeeVault address + +If you want a deterministic address across chains, compute the CREATE2 address and use that address in `alloc`: + +```bash +export OWNER=0xYourOwnerOrAdminProxy +export SALT=0x0000000000000000000000000000000000000000000000000000000000000001 +export DEPLOYER=0xYourDeployerAddress +export DESTINATION_DOMAIN=1234 +export RECIPIENT_ADDRESS=0x0000000000000000000000000000000000000000000000000000000000000000 +export MINIMUM_AMOUNT=0 +export CALL_FEE=0 +export BRIDGE_SHARE_BPS=10000 +export OTHER_RECIPIENT=0x0000000000000000000000000000000000000000 + +forge script script/DeployFeeVault.s.sol:ComputeFeeVaultAddress +``` + +If you do not care about CREATE2 determinism, pick any address and use it in `alloc`. + +### 2. Get the runtime bytecode + +Use the deployed (runtime) bytecode in genesis: + +```bash +forge inspect FeeVault deployedBytecode +``` + +You can also generate the alloc snippet (including code + storage) with the helper script: + +```bash +# Required +export OWNER=0xYourOwnerOrAdminProxy + +# Optional but recommended for a deterministic address +export DEPLOYER=0xYourDeployerAddress +export SALT=0x0000000000000000000000000000000000000000000000000000000000000001 + +# If you are not using CREATE2, set the address explicitly +export FEE_VAULT_ADDRESS=0xYourFeeVaultAddress + +# Optional configuration (defaults to zero) +export DESTINATION_DOMAIN=1234 +export RECIPIENT_ADDRESS=0x0000000000000000000000000000000000000000000000000000000000000000 +export MINIMUM_AMOUNT=0 +export CALL_FEE=0 +export BRIDGE_SHARE_BPS=10000 +export OTHER_RECIPIENT=0x0000000000000000000000000000000000000000 +export HYP_NATIVE_MINTER=0x0000000000000000000000000000000000000000 + +forge script script/GenerateFeeVaultAlloc.s.sol -vvv +``` + +### 3. Set storage slots in alloc + +Storage layout is derived from declaration order in `FeeVault.sol`: + +| Slot | Variable | Encoding | +|------|----------|----------| +| `0x0` | `hypNativeMinter` | Address (20 bytes, left-padded) | +| `0x1` | `owner` + `destinationDomain` | `0x0000000000000000` | +| `0x2` | `recipientAddress` | bytes32 | +| `0x3` | `minimumAmount` | uint256 | +| `0x4` | `callFee` | uint256 | +| `0x5` | `otherRecipient` | Address (20 bytes, left-padded) | +| `0x6` | `bridgeShareBps` | uint256 | + +Notes: + +- `owner` must be non-zero, otherwise no one can administer the vault. +- The constructor default (`bridgeShareBps = 10000 when 0`) does **not** apply at genesis. Set `0x2710` (10000) explicitly if you want 100% bridging. The helper script applies this default for you when `BRIDGE_SHARE_BPS=0`. +- `hypNativeMinter` can be zero at genesis, but it must be set before calling `sendToCelestia()`. + +Example alloc entry (address key without `0x`): + +```json +{ + "alloc": { + "": { + "balance": "0x0", + "code": "0x", + "storage": { + "0x0": "0x0000000000000000000000001111111111111111111111111111111111111111", + "0x1": "0x0000000000000000000004d22222222222222222222222222222222222222222", + "0x2": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x3": "0x0", + "0x4": "0x0", + "0x5": "0x0000000000000000000000000000000000000000", + "0x6": "0x2710" + } + } + } +} +``` + +### 4. Verify after genesis + +Once the node is running with your genesis file, verify the configuration on-chain: + +```bash +# Check runtime code exists +cast code --rpc-url + +# Inspect full config in one call +cast call \ + "getConfig()(address,uint32,bytes32,uint256,uint256,uint256,address,address)" \ + --rpc-url + +# Or read individual storage slots (optional) +cast storage 0x0 --rpc-url +cast storage 0x1 --rpc-url +``` + +### 5. Wire base fee redirect (optional) + +To route base fees into FeeVault from genesis, set `ev_reth.baseFeeSink` to the FeeVault address and `baseFeeRedirectActivationHeight` to `0` in your chainspec (see `README.md` for the full chainspec example). diff --git a/docs/eip1559-configuration.md b/docs/eip1559-configuration.md new file mode 100644 index 0000000..7267f1b --- /dev/null +++ b/docs/eip1559-configuration.md @@ -0,0 +1,85 @@ +# EIP-1559 Configuration for 100ms Blocks + +## TLDR + +Recommendations for gas limit, base fee, and EIP-1559 parameters +for mainnet launch with 100ms blocks. + +## Recommended Configuration + +| Parameter | Recommendation | Rationale | +| --- | --- | --- | +| **Gas Limit** | 50M per block | Balances throughput (500M gas/sec) with execution time constraints | +| **Initial Base Fee** | 0.1 ntia | Provides spam protection | +| **Minimum Base Fee** | 0.1 ntia | Prevents drift to zero, ensures sustainable economics | +| **EIP1559 Denominator** | 5000 | Smooth fee adjustments: +/-0.02%/block, +/-0.2% per second, same as OP networks | +| **EIP1559 Elasticity Multiplier** | 10 | Lower per block target for eip1559 while allowing 10x burst capacity | + +## Why These Numbers + +### Gas Limit: 50M per block + +- **Throughput:** 500M gas/second theoretical max +- Start here, can scale up later if needed + +### Base Fee: 0.1 ntia minimum/initial + +- **DA coverage:** Should ensure gas fees cover data availability costs, but will have to run analysis post-launch +- **Spam protection:** Makes attacks expensive while keeping normal use cheap +- **Testnet lesson:** Without a minimum, testnet dropped to 7 atia (wei) + +### Denominator: 5000 + +**Problem with default:** +/-12.5% per block = extreme volatility at 100ms + +- 5 full blocks (0.5 sec) is a 61% fee increase +- Unpredictable costs, wallet estimations out of range between simulation and submission, poor UX + +**With 5000:** + +- +/-0.02% per block; +/-0.2% per second; +/-2.4% per 12 seconds +- Smoother changes over time +- Similar to Optimism's adjustment rate + +### Elasticity: 10 + +**Problem with default:** Target would be 25M gas/block (50% of max) + +- Requires 250M gas/second sustained to maintain base fee +- Unrealistic with bursty 100ms traffic +- Causes constant downward pressure; fees drift to zero + +## Comparison to Other Chains + +| Chain | Block Time | Block Gas Limit | Denominator | Elasticity | +| --- | --- | --- | --- | --- | +| Ethereum | 12s | 30M | 8 | 2 | +| Base | 2s | 300M | 250 | 6 | +| **Eden (proposed)** | 100ms | 50M | 5000 | 10 | + +## Configuration Mapping (ev-reth) + +The example chainspec at `etc/ev-reth-genesis.json` already uses the recommended defaults. + +```json +{ + "gasLimit": "0x2faf080", + "baseFeePerGas": "0x16345785d8a0000", + "config": { + "londonBlock": 0, + "evolve": { + "baseFeeMaxChangeDenominator": 5000, + "baseFeeElasticityMultiplier": 10, + "initialBaseFeePerGas": 100000000000000000 + } + } +} +``` + +Notes: + +- `baseFeePerGas` is the genesis base fee; `initialBaseFeePerGas` sets the same value when + `londonBlock` is `0`. Keep them consistent. +- The values above assume 18 decimals for `ntia` (0.1 ntia = 100000000000000000). +- ev-reth does not enforce a protocol-level minimum base fee. If you need a floor, use the + txpool admission guard (e.g., `--txpool.minimal-protocol-basefee`) and align wallet defaults. diff --git a/etc/ev-reth-genesis.json b/etc/ev-reth-genesis.json index 024e0c7..bd97741 100644 --- a/etc/ev-reth-genesis.json +++ b/etc/ev-reth-genesis.json @@ -19,6 +19,9 @@ "evolve": { "baseFeeSink": "0x00000000000000000000000000000000000000fe", "baseFeeRedirectActivationHeight": 0, + "baseFeeMaxChangeDenominator": 5000, + "baseFeeElasticityMultiplier": 10, + "initialBaseFeePerGas": 100000000000000000, "mintAdmin": "0x000000000000000000000000000000000000Ad00", "mintPrecompileActivationHeight": 0, "contractSizeLimit": 131072, @@ -26,7 +29,8 @@ } }, "difficulty": "0x1", - "gasLimit": "0x1c9c380", + "gasLimit": "0x2faf080", + "baseFeePerGas": "0x16345785d8a0000", "alloc": { "000000000000000000000000000000000000Ad00": { "balance": "0x0",