From b6717fdd648966538ce1ccf988fdd048a4dabd85 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 8 Jan 2026 16:48:04 +0100 Subject: [PATCH 1/5] add proxy contract --- .../script/GenerateAdminProxyAlloc.s.sol | 90 ++++ contracts/src/AdminProxy.sol | 152 +++++++ contracts/test/AdminProxy.t.sol | 423 ++++++++++++++++++ 3 files changed, 665 insertions(+) create mode 100644 contracts/script/GenerateAdminProxyAlloc.s.sol create mode 100644 contracts/src/AdminProxy.sol create mode 100644 contracts/test/AdminProxy.t.sol diff --git a/contracts/script/GenerateAdminProxyAlloc.s.sol b/contracts/script/GenerateAdminProxyAlloc.s.sol new file mode 100644 index 0000000..89742fc --- /dev/null +++ b/contracts/script/GenerateAdminProxyAlloc.s.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Script, console} from "forge-std/Script.sol"; +import {AdminProxy} from "../src/AdminProxy.sol"; + +/// @title GenerateAdminProxyAlloc +/// @notice Generates genesis alloc JSON for deploying AdminProxy at a deterministic address +/// @dev Run with: forge script script/GenerateAdminProxyAlloc.s.sol -vvv +/// +/// This script outputs the bytecode and storage layout needed to deploy AdminProxy +/// in the genesis block. The contract is deployed with owner = address(0), allowing +/// the first caller to claim ownership post-genesis. +/// +/// Usage: +/// 1. Run this script to get the bytecode +/// 2. Add to genesis.json alloc section at desired address (e.g., 0x...AD00) +/// 3. Set that address as mintAdmin in chainspec config +contract GenerateAdminProxyAlloc is Script { + // Suggested deterministic address for AdminProxy + // Using a memorable address in the precompile-adjacent range + address constant SUGGESTED_ADDRESS = 0x000000000000000000000000000000000000Ad00; + + function run() external { + // Deploy to get runtime bytecode + AdminProxy proxy = new AdminProxy(); + + // Get runtime bytecode (not creation code) + bytes memory runtimeCode = address(proxy).code; + + console.log("========== AdminProxy Genesis Alloc =========="); + console.log(""); + console.log("Suggested address:", SUGGESTED_ADDRESS); + console.log(""); + console.log("Add this to your genesis.json 'alloc' section:"); + console.log(""); + console.log("{"); + console.log(' "alloc": {'); + console.log(' "000000000000000000000000000000000000Ad00": {'); + console.log(' "balance": "0x0",'); + console.log(' "code": "0x%s",', vm.toString(runtimeCode)); + console.log(' "storage": {}'); + console.log(" }"); + console.log(" }"); + console.log("}"); + console.log(""); + console.log("Then update chainspec config:"); + console.log(""); + console.log('{'); + console.log(' "config": {'); + console.log(' "evolve": {'); + console.log(' "mintAdmin": "0x000000000000000000000000000000000000Ad00",'); + console.log(' "mintPrecompileActivationHeight": 0'); + console.log(" }"); + console.log(" }"); + console.log("}"); + console.log(""); + console.log("=============================================="); + console.log(""); + console.log("Post-genesis steps:"); + console.log("1. Call claimOwnership() from desired EOA"); + console.log("2. Deploy multisig (e.g., Safe)"); + console.log("3. Call transferOwnership(multisigAddress)"); + console.log("4. From multisig, call acceptOwnership()"); + console.log(""); + + // Also output raw values for programmatic use + console.log("Raw bytecode length:", runtimeCode.length); + } +} + +/// @title GenerateAdminProxyAllocJSON +/// @notice Outputs just the JSON snippet for easy copy-paste +contract GenerateAdminProxyAllocJSON is Script { + function run() external { + AdminProxy proxy = new AdminProxy(); + bytes memory runtimeCode = address(proxy).code; + + // Output minimal JSON that can be merged into genesis + string memory json = string( + abi.encodePacked( + '{"000000000000000000000000000000000000Ad00":{"balance":"0x0","code":"0x', + vm.toString(runtimeCode), + '","storage":{}}}' + ) + ); + + console.log(json); + } +} diff --git a/contracts/src/AdminProxy.sol b/contracts/src/AdminProxy.sol new file mode 100644 index 0000000..1961de8 --- /dev/null +++ b/contracts/src/AdminProxy.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title AdminProxy +/// @notice A proxy contract for managing admin rights to precompiles and other contracts. +/// @dev Deployed at genesis with zero owner, allowing first-come claim. Supports two-step +/// ownership transfer for safe handoff to multisigs or other governance contracts. +/// +/// This contract solves the bootstrap problem where admin addresses (e.g., multisigs) +/// are not known at genesis time. The proxy is set as admin in the chainspec, and +/// ownership can be claimed and transferred post-genesis. +/// +/// Usage: +/// 1. Deploy at genesis with zero owner (via genesis alloc) +/// 2. Set proxy address as `mintAdmin` in chainspec and as FeeVault owner +/// 3. Post-genesis: call claimOwnership() to become initial owner +/// 4. Deploy multisig, then transferOwnership() -> acceptOwnership() to hand off +contract AdminProxy { + /// @notice Current owner of the proxy + address public owner; + + /// @notice Pending owner for two-step transfer + address public pendingOwner; + + /// @notice Emitted when ownership transfer is initiated + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + + /// @notice Emitted when ownership transfer is completed + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Emitted when a call is executed through the proxy + event Executed(address indexed target, bytes data, bytes result); + + /// @notice Thrown when caller is not the owner + error NotOwner(); + + /// @notice Thrown when caller is not the pending owner + error NotPendingOwner(); + + /// @notice Thrown when a call to target contract fails + error CallFailed(bytes reason); + + /// @notice Thrown when array lengths don't match in batch operations + error LengthMismatch(); + + /// @notice Thrown when trying to set zero address as pending owner + error ZeroAddress(); + + modifier onlyOwner() { + if (msg.sender != owner) revert NotOwner(); + _; + } + + /// @notice Initialize with zero owner - first caller can claim ownership + constructor() { + owner = address(0); + } + + /// @notice Claim ownership when owner is zero (genesis bootstrap) + /// @dev Can only be called once, when owner is address(0) + function claimOwnership() external { + if (owner != address(0)) revert NotOwner(); + owner = msg.sender; + emit OwnershipTransferred(address(0), msg.sender); + } + + /// @notice Start two-step ownership transfer + /// @param newOwner Address of the new owner (e.g., multisig) + function transferOwnership(address newOwner) external onlyOwner { + if (newOwner == address(0)) revert ZeroAddress(); + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + /// @notice Complete two-step ownership transfer + /// @dev Must be called by the pending owner + function acceptOwnership() external { + if (msg.sender != pendingOwner) revert NotPendingOwner(); + emit OwnershipTransferred(owner, msg.sender); + owner = msg.sender; + pendingOwner = address(0); + } + + /// @notice Cancel pending ownership transfer + function cancelTransfer() external onlyOwner { + pendingOwner = address(0); + } + + /// @notice Execute a call to any target contract + /// @param target Address of the contract to call + /// @param data Calldata to send + /// @return result The return data from the call + /// @dev Use this to call admin functions on FeeVault, precompiles, etc. + /// + /// Example - Add address to mint precompile allowlist: + /// execute(MINT_PRECOMPILE, abi.encodeCall(IMintPrecompile.addToAllowList, (account))) + /// + /// Example - Transfer FeeVault ownership: + /// execute(feeVault, abi.encodeCall(FeeVault.transferOwnership, (newOwner))) + function execute(address target, bytes calldata data) external onlyOwner returns (bytes memory result) { + (bool success, bytes memory returnData) = target.call(data); + if (!success) { + revert CallFailed(returnData); + } + emit Executed(target, data, returnData); + return returnData; + } + + /// @notice Execute multiple calls in a single transaction + /// @param targets Array of contract addresses to call + /// @param datas Array of calldata for each call + /// @return results Array of return data from each call + /// @dev Useful for batch operations like adding multiple addresses to allowlist + function executeBatch(address[] calldata targets, bytes[] calldata datas) + external + onlyOwner + returns (bytes[] memory results) + { + if (targets.length != datas.length) revert LengthMismatch(); + + results = new bytes[](targets.length); + for (uint256 i = 0; i < targets.length; i++) { + (bool success, bytes memory returnData) = targets[i].call(datas[i]); + if (!success) { + revert CallFailed(returnData); + } + emit Executed(targets[i], datas[i], returnData); + results[i] = returnData; + } + } + + /// @notice Execute a call with ETH value + /// @param target Address of the contract to call + /// @param data Calldata to send + /// @param value Amount of ETH to send + /// @return result The return data from the call + function executeWithValue(address target, bytes calldata data, uint256 value) + external + onlyOwner + returns (bytes memory result) + { + (bool success, bytes memory returnData) = target.call{value: value}(data); + if (!success) { + revert CallFailed(returnData); + } + emit Executed(target, data, returnData); + return returnData; + } + + /// @notice Receive ETH (needed for executeWithValue) + receive() external payable {} +} diff --git a/contracts/test/AdminProxy.t.sol b/contracts/test/AdminProxy.t.sol new file mode 100644 index 0000000..bf2a9be --- /dev/null +++ b/contracts/test/AdminProxy.t.sol @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {AdminProxy} from "../src/AdminProxy.sol"; +import {FeeVault} from "../src/FeeVault.sol"; + +/// @dev Mock contract to test AdminProxy execute functionality +contract MockTarget { + uint256 public value; + address public lastCaller; + + error CustomError(string message); + + function setValue(uint256 _value) external { + value = _value; + lastCaller = msg.sender; + } + + function getValue() external view returns (uint256) { + return value; + } + + function revertWithMessage() external pure { + revert("MockTarget: intentional revert"); + } + + function revertWithCustomError() external pure { + revert CustomError("custom error"); + } + + function payableFunction() external payable { + value = msg.value; + } +} + +/// @dev Mock mint precompile interface for testing +contract MockMintPrecompile { + mapping(address => bool) public allowlist; + address public admin; + + error NotAdmin(); + + constructor(address _admin) { + admin = _admin; + } + + modifier onlyAdmin() { + if (msg.sender != admin) revert NotAdmin(); + _; + } + + function addToAllowList(address account) external onlyAdmin { + allowlist[account] = true; + } + + function removeFromAllowList(address account) external onlyAdmin { + allowlist[account] = false; + } +} + +contract AdminProxyTest is Test { + AdminProxy public proxy; + MockTarget public target; + MockMintPrecompile public mintPrecompile; + + address public alice = address(0x1); + address public bob = address(0x2); + address public multisig = address(0x3); + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event Executed(address indexed target, bytes data, bytes result); + + function setUp() public { + proxy = new AdminProxy(); + target = new MockTarget(); + } + + // ============ Ownership Tests ============ + + function test_InitialOwnerIsZero() public view { + assertEq(proxy.owner(), address(0)); + } + + function test_ClaimOwnership() public { + vm.prank(alice); + vm.expectEmit(true, true, false, false); + emit OwnershipTransferred(address(0), alice); + proxy.claimOwnership(); + + assertEq(proxy.owner(), alice); + } + + function test_ClaimOwnership_RevertWhenAlreadyClaimed() public { + vm.prank(alice); + proxy.claimOwnership(); + + vm.prank(bob); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.claimOwnership(); + } + + function test_TransferOwnership_TwoStep() public { + // Alice claims ownership + vm.prank(alice); + proxy.claimOwnership(); + + // Alice initiates transfer to bob + vm.prank(alice); + vm.expectEmit(true, true, false, false); + emit OwnershipTransferStarted(alice, bob); + proxy.transferOwnership(bob); + + assertEq(proxy.owner(), alice); // Still alice + assertEq(proxy.pendingOwner(), bob); + + // Bob accepts + vm.prank(bob); + vm.expectEmit(true, true, false, false); + emit OwnershipTransferred(alice, bob); + proxy.acceptOwnership(); + + assertEq(proxy.owner(), bob); + assertEq(proxy.pendingOwner(), address(0)); + } + + function test_TransferOwnership_RevertZeroAddress() public { + vm.prank(alice); + proxy.claimOwnership(); + + vm.prank(alice); + vm.expectRevert(AdminProxy.ZeroAddress.selector); + proxy.transferOwnership(address(0)); + } + + function test_AcceptOwnership_RevertNotPending() public { + vm.prank(alice); + proxy.claimOwnership(); + + vm.prank(alice); + proxy.transferOwnership(bob); + + // Charlie tries to accept + address charlie = address(0x4); + vm.prank(charlie); + vm.expectRevert(AdminProxy.NotPendingOwner.selector); + proxy.acceptOwnership(); + } + + function test_CancelTransfer() public { + vm.prank(alice); + proxy.claimOwnership(); + + vm.prank(alice); + proxy.transferOwnership(bob); + assertEq(proxy.pendingOwner(), bob); + + vm.prank(alice); + proxy.cancelTransfer(); + assertEq(proxy.pendingOwner(), address(0)); + + // Bob can no longer accept + vm.prank(bob); + vm.expectRevert(AdminProxy.NotPendingOwner.selector); + proxy.acceptOwnership(); + } + + function test_TransferOwnership_RevertNotOwner() public { + vm.prank(alice); + proxy.claimOwnership(); + + vm.prank(bob); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.transferOwnership(bob); + } + + // ============ Execute Tests ============ + + function test_Execute() public { + vm.prank(alice); + proxy.claimOwnership(); + + bytes memory data = abi.encodeCall(MockTarget.setValue, (42)); + + vm.prank(alice); + vm.expectEmit(true, false, false, false); + emit Executed(address(target), data, ""); + proxy.execute(address(target), data); + + assertEq(target.value(), 42); + assertEq(target.lastCaller(), address(proxy)); // Proxy is the caller + } + + function test_Execute_ReturnsData() public { + vm.prank(alice); + proxy.claimOwnership(); + + // First set a value + vm.prank(alice); + proxy.execute(address(target), abi.encodeCall(MockTarget.setValue, (123))); + + // Then get it + vm.prank(alice); + bytes memory result = proxy.execute(address(target), abi.encodeCall(MockTarget.getValue, ())); + + uint256 decoded = abi.decode(result, (uint256)); + assertEq(decoded, 123); + } + + function test_Execute_RevertNotOwner() public { + vm.prank(alice); + proxy.claimOwnership(); + + vm.prank(bob); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.execute(address(target), abi.encodeCall(MockTarget.setValue, (42))); + } + + function test_Execute_PropagatesRevert() public { + vm.prank(alice); + proxy.claimOwnership(); + + // The revert data is ABI-encoded as Error(string), not raw bytes + bytes memory expectedRevertData = abi.encodeWithSignature("Error(string)", "MockTarget: intentional revert"); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(AdminProxy.CallFailed.selector, expectedRevertData)); + proxy.execute(address(target), abi.encodeCall(MockTarget.revertWithMessage, ())); + } + + // ============ ExecuteBatch Tests ============ + + function test_ExecuteBatch() public { + vm.prank(alice); + proxy.claimOwnership(); + + MockTarget target2 = new MockTarget(); + + address[] memory targets = new address[](2); + targets[0] = address(target); + targets[1] = address(target2); + + bytes[] memory datas = new bytes[](2); + datas[0] = abi.encodeCall(MockTarget.setValue, (100)); + datas[1] = abi.encodeCall(MockTarget.setValue, (200)); + + vm.prank(alice); + proxy.executeBatch(targets, datas); + + assertEq(target.value(), 100); + assertEq(target2.value(), 200); + } + + function test_ExecuteBatch_RevertLengthMismatch() public { + vm.prank(alice); + proxy.claimOwnership(); + + address[] memory targets = new address[](2); + bytes[] memory datas = new bytes[](1); + + vm.prank(alice); + vm.expectRevert(AdminProxy.LengthMismatch.selector); + proxy.executeBatch(targets, datas); + } + + function test_ExecuteBatch_RevertOnAnyFailure() public { + vm.prank(alice); + proxy.claimOwnership(); + + address[] memory targets = new address[](2); + targets[0] = address(target); + targets[1] = address(target); + + bytes[] memory datas = new bytes[](2); + datas[0] = abi.encodeCall(MockTarget.setValue, (100)); + datas[1] = abi.encodeCall(MockTarget.revertWithMessage, ()); + + // The revert data is ABI-encoded as Error(string), not raw bytes + bytes memory expectedRevertData = abi.encodeWithSignature("Error(string)", "MockTarget: intentional revert"); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(AdminProxy.CallFailed.selector, expectedRevertData)); + proxy.executeBatch(targets, datas); + } + + // ============ ExecuteWithValue Tests ============ + + function test_ExecuteWithValue() public { + vm.prank(alice); + proxy.claimOwnership(); + + // Fund the proxy + vm.deal(address(proxy), 1 ether); + + vm.prank(alice); + proxy.executeWithValue(address(target), abi.encodeCall(MockTarget.payableFunction, ()), 0.5 ether); + + assertEq(target.value(), 0.5 ether); + assertEq(address(proxy).balance, 0.5 ether); + } + + function test_ReceiveEth() public { + (bool success,) = address(proxy).call{value: 1 ether}(""); + assertTrue(success); + assertEq(address(proxy).balance, 1 ether); + } + + // ============ Integration Tests ============ + + function test_Integration_ProxyAsMintPrecompileAdmin() public { + // Deploy mint precompile with proxy as admin + mintPrecompile = new MockMintPrecompile(address(proxy)); + + // Alice claims proxy ownership + vm.prank(alice); + proxy.claimOwnership(); + + // Alice uses proxy to add bob to allowlist + vm.prank(alice); + proxy.execute(address(mintPrecompile), abi.encodeCall(MockMintPrecompile.addToAllowList, (bob))); + + assertTrue(mintPrecompile.allowlist(bob)); + + // Direct call fails (alice is not admin, proxy is) + vm.prank(alice); + vm.expectRevert(MockMintPrecompile.NotAdmin.selector); + mintPrecompile.addToAllowList(address(0x5)); + } + + function test_Integration_TransferToMultisig() public { + // Simulate genesis -> multisig flow + mintPrecompile = new MockMintPrecompile(address(proxy)); + + // 1. Alice (EOA) claims ownership post-genesis + vm.prank(alice); + proxy.claimOwnership(); + + // 2. Alice does some admin work + vm.prank(alice); + proxy.execute(address(mintPrecompile), abi.encodeCall(MockMintPrecompile.addToAllowList, (bob))); + + // 3. Multisig is deployed (simulated) + // 4. Alice transfers to multisig + vm.prank(alice); + proxy.transferOwnership(multisig); + + // 5. Multisig accepts + vm.prank(multisig); + proxy.acceptOwnership(); + + assertEq(proxy.owner(), multisig); + + // 6. Multisig can now admin + vm.prank(multisig); + proxy.execute(address(mintPrecompile), abi.encodeCall(MockMintPrecompile.removeFromAllowList, (bob))); + + assertFalse(mintPrecompile.allowlist(bob)); + + // 7. Alice can no longer admin + vm.prank(alice); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.execute(address(mintPrecompile), abi.encodeCall(MockMintPrecompile.addToAllowList, (alice))); + } + + function test_Integration_ProxyAsFeeVaultOwner() public { + // Deploy FeeVault with proxy as owner + FeeVault vault = new FeeVault( + address(proxy), // proxy is owner + 1234, + bytes32(uint256(0xbeef)), + 1 ether, + 0.1 ether, + 10000, + address(0x99) + ); + + // Alice claims proxy ownership + vm.prank(alice); + proxy.claimOwnership(); + + // Alice uses proxy to update FeeVault config + vm.prank(alice); + proxy.execute(address(vault), abi.encodeCall(FeeVault.setMinimumAmount, (2 ether))); + + assertEq(vault.minimumAmount(), 2 ether); + + // Direct call fails + vm.prank(alice); + vm.expectRevert("FeeVault: caller is not the owner"); + vault.setMinimumAmount(3 ether); + } + + function test_Integration_BatchAllowlistUpdates() public { + mintPrecompile = new MockMintPrecompile(address(proxy)); + + vm.prank(alice); + proxy.claimOwnership(); + + // Batch add multiple addresses to allowlist + address[] memory targets = new address[](3); + bytes[] memory datas = new bytes[](3); + + address user1 = address(0x10); + address user2 = address(0x11); + address user3 = address(0x12); + + targets[0] = address(mintPrecompile); + targets[1] = address(mintPrecompile); + targets[2] = address(mintPrecompile); + + datas[0] = abi.encodeCall(MockMintPrecompile.addToAllowList, (user1)); + datas[1] = abi.encodeCall(MockMintPrecompile.addToAllowList, (user2)); + datas[2] = abi.encodeCall(MockMintPrecompile.addToAllowList, (user3)); + + vm.prank(alice); + proxy.executeBatch(targets, datas); + + assertTrue(mintPrecompile.allowlist(user1)); + assertTrue(mintPrecompile.allowlist(user2)); + assertTrue(mintPrecompile.allowlist(user3)); + } +} From abd070e9cc8d955e44cfc103e62e06988c440c22 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 8 Jan 2026 16:50:11 +0100 Subject: [PATCH 2/5] doc --- docs/contracts/admin_proxy.md | 320 ++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 docs/contracts/admin_proxy.md diff --git a/docs/contracts/admin_proxy.md b/docs/contracts/admin_proxy.md new file mode 100644 index 0000000..adabafc --- /dev/null +++ b/docs/contracts/admin_proxy.md @@ -0,0 +1,320 @@ +# AdminProxy Design & Use Case + +## Overview + +The `AdminProxy` is a smart contract that solves the bootstrap problem for admin addresses at genesis. It acts as an intermediary owner/admin for other contracts and precompiles when the final admin (e.g., a multisig) is not known at genesis time. + +## Problem Statement + +Several components in ev-reth require admin addresses configured at genesis: + +1. **Mint Precompile**: Requires `mintAdmin` in chainspec to manage the allowlist +2. **FeeVault**: Requires an `owner` address in its constructor + +The challenge: these admin addresses often need to be multisigs (like Safe) for security, but multisigs cannot be deployed at genesis because they require transactions to be created. + +## Solution + +Deploy `AdminProxy` at genesis with `owner = address(0)`. Post-genesis: + +1. An EOA claims ownership +2. The multisig is deployed +3. Ownership is transferred to the multisig via two-step transfer + +The proxy then forwards admin calls to the underlying contracts/precompiles. + +## Architecture + +``` + ┌─────────────────┐ + │ Multisig │ + │ (Safe, etc) │ + └────────┬────────┘ + │ + │ owns + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ AdminProxy │ +│ - owner: address (initially 0, then EOA, then multisig) │ +│ - execute(target, data): forward calls │ +│ - executeBatch(targets, datas): batch operations │ +└──────────────┬────────────────────────┬─────────────────────┘ + │ │ + │ admin calls │ owner calls + ▼ ▼ + ┌──────────────────┐ ┌──────────────────┐ + │ Mint Precompile │ │ FeeVault │ + │ (0xF100) │ │ │ + └──────────────────┘ └──────────────────┘ +``` + +## Genesis Configuration + +This section provides detailed instructions for deploying AdminProxy at genesis. + +### Step 1: Build the Contract + +```bash +cd contracts +forge build +``` + +### Step 2: Get the Runtime Bytecode + +There are two ways to get the bytecode: + +**Option A: Use the helper script** + +```bash +forge script script/GenerateAdminProxyAlloc.s.sol -vvv +``` + +This outputs the complete alloc entry you can copy into your genesis file. + +**Option B: Get bytecode directly from artifacts** + +After building, the runtime bytecode is in the compiled artifacts: + +```bash +# Extract just the deployed bytecode (not creation code) +cat out/AdminProxy.sol/AdminProxy.json | jq -r '.deployedBytecode.object' +``` + +This outputs the hex string starting with `0x608060...`. + +### Step 3: Create the Genesis Alloc Entry + +The genesis `alloc` section pre-deploys contracts at specific addresses. For AdminProxy: + +```json +{ + "alloc": { + "000000000000000000000000000000000000Ad00": { + "balance": "0x0", + "code": "0x608060405234801561001057600080fd5b50600436106100935760003560e01c80638da5cb5b116100665780638da5cb5b146100fa578063b0e21e8a1461010b578063e30c39781461011e578063f2fde38b1461012f578063fe0d94c11461014257600080fd5b80631a69523014610098578063715018a6146100ad57806374e6310e146100b557806379ba5097146100f2575b600080fd5b6100ab6100a63660046108d5565b610155565b005b6100ab6101d5565b6100c86100c33660046108f7565b6101e9565b604080516001600160a01b0390931683526020830191909152015b60405180910390f35b6100ab610283565b6000546001600160a01b03166100c8565b6100ab610119366004610938565b6102f8565b6001546001600160a01b03166100c8565b6100ab61013d3660046108d5565b610410565b6100ab6101503660046109ab565b610481565b61015d610528565b6001600160a01b03811661018457604051631e4fbdf760e01b815260040160405180910390fd5b600180546001600160a01b0319166001600160a01b0383169081179091556000546040516001600160a01b03909116907f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270090600090a350565b6101dd610528565b6101e76000610555565b565b60008061021d846040518060400160405280600e81526020016d2737ba1030b63637bbb2b2103a3960911b8152506105a5565b9050600080856001600160a01b031683866040516102449291906001600160a01b03929092168252602082015260400190565b6000604051808303816000875af1925050503d8060008114610282576040519150601f19603f3d011682016040523d82523d6000602084013e610287565b606091505b5091509150816102ac576102ae604051806060016040528060228152602001610b2f60229139836105d1565b505b6040805180820182526001600160a01b038089168252602080830188905283518581529182018690528451928301939093529051909116907f6e9b6e3f1f8e21e9d5e8f5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e89181900360600190a25090925050509250929050565b6001546001600160a01b031633146102e85760405163118cdaa760e01b81523360048201526024015b60405180910390fd5b6001805460006001600160a01b0319918216178255805482166001600160a01b03831690811782556040519192909116907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908490a350565b610300610528565b82811461032057604051634ec4810560e11b815260040160405180910390fd5b60005b838110156104095760008086868481811061034057610340610a4d565b90506020020160208101906103559190610938565b6001600160a01b031685858581811061037057610370610a4d565b905060200281019061038291906109f4565b604051610390929190610a3d565b6000604051808303816000865af19150503d80600081146103cd576040519150601f19603f3d011682016040523d82523d6000602084013e6103d2565b606091505b5091509150816103ff576103f9604051806060016040528060228152602001610b2f60229139836105d1565b50610400565b5b50600101610323565b5050505050565b610418610528565b6001600160a01b03811661043f57604051631e4fbdf760e01b815260040160405180910390fd5b600180546001600160a01b0319166001600160a01b0383169081179091556000546040516001600160a01b03909116907f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270090600090a350565b610489610528565b6000826001600160a01b031682846040516104a49190610a63565b60006040518083038185875af1925050503d80600081146104e1576040519150601f19603f3d011682016040523d82523d6000602084013e6104e6565b606091505b505090508061052257610522604051806060016040528060228152602001610b2f6022913960405180602001604052806000815250905090506105d1565b50505050565b6000546001600160a01b031633146101e75760405163118cdaa760e01b81523360048201526024016102df565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6060828260405160200161057b92919091825260601b6bffffffffffffffffffffffff1916602082015260340190565b604051602081830303815290604052905092915050565b60606105e183836040518060400160405280601e81526020017f416464726573733a206c6f772d6c6576656c2063616c6c206661696c65640000815250610648565b9392505050565b81516000036105f8575050565b8060000361060557505050565b815160208301fd5b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff811182821017156106485761064861060d565b604052919050565b919050565b600067ffffffffffffffff8211156106775761067761060d565b50601f01601f191660200190565b600082601f83011261069657600080fd5b81356106a96106a48261065d565b610623565b8181528460208386010111156106be57600080fd5b816020850160208301376000918101602001919091529392505050565b6000806000606084860312156106f057600080fd5b83356001600160a01b038116811461070757600080fd5b925060208401359150604084013567ffffffffffffffff81111561072a57600080fd5b61073686828701610685565b9150509250925092565b60008083601f84011261075257600080fd5b50813567ffffffffffffffff81111561076a57600080fd5b6020830191508360208260051b850101111561078557600080fd5b9250929050565b600080600080604085870312156107a257600080fd5b843567ffffffffffffffff808211156107ba57600080fd5b6107c688838901610740565b909650945060208701359150808211156107df57600080fd5b506107ec87828801610740565b95989497509550505050565b60005b838110156108135781810151838201526020016107fb565b50506000910152565b600081518084526108348160208601602086016107f8565b601f01601f19169290920160200192915050565b6001600160a01b038716815260208101869052604081018590526060810184905260c06080820181905260009061088190830185610823565b82810360a08401526108938185610823565b9998505050505050505050565b6000602082840312156108b257600080fd5b5035919050565b6001600160a01b03811681146108ce57600080fd5b50565b6000602082840312156108e357600080fd5b81356108ee816108b9565b9392505050565b6000806040838503121561090857600080fd5b8235610913816108b9565b9150602083013567ffffffffffffffff81111561092f57600080fd5b61093b85828601610685565b9150509250929050565b6000806000806040858703121561095b57600080fd5b843567ffffffffffffffff8082111561097357600080fd5b61097f88838901610740565b9650602087013591508082111561099557600080fd5b506107ec87828801610740565b634e487b7160e01b600052603260045260246000fd5b6000602082840312156109c957600080fd5b81356001600160a01b03811681146108ee57600080fd5b8183823760009101908152919050565b600082516109ff8184602087016107f8565b9190910192915050565b60008151808452610a218160208601602086016107f8565b601f01601f19169290920160200192915050565b6020815260006105e16020830184610a0956fe416464726573733a2063616c6c206661696c656420776974686f757420726576657274696e67a2646970667358221220...", + "storage": {} + } + } +} +``` + +**Important notes about the alloc entry:** + +1. **Address format**: The address key does NOT have the `0x` prefix in the alloc section +2. **Code format**: The code value MUST have the `0x` prefix +3. **Storage**: Empty `{}` because AdminProxy initializes `owner = address(0)` and `pendingOwner = address(0)`, which are the default zero values (no explicit storage needed) + +### Step 4: Complete Genesis File Example + +Here's a complete example showing how AdminProxy fits into the full genesis file: + +```json +{ + "config": { + "chainId": 1, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "parisBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "evolve": { + "baseFeeSink": "0x00000000000000000000000000000000000000fe", + "baseFeeRedirectActivationHeight": 0, + "mintAdmin": "0x000000000000000000000000000000000000Ad00", + "mintPrecompileActivationHeight": 0, + "contractSizeLimit": 131072, + "contractSizeLimitActivationHeight": 0 + } + }, + "difficulty": "0x1", + "gasLimit": "0x1c9c380", + "alloc": { + "000000000000000000000000000000000000Ad00": { + "balance": "0x0", + "code": "0x", + "storage": {} + } + } +} +``` + +### Step 5: Verify the Setup + +After creating your genesis file, you can verify the AdminProxy is correctly configured: + +1. **Start the node** with your genesis file +2. **Query the contract code** at the proxy address to confirm deployment: + + ```bash + cast code 0x000000000000000000000000000000000000Ad00 --rpc-url + ``` + +3. **Check owner is zero** (ready for claiming): + + ```bash + cast call 0x000000000000000000000000000000000000Ad00 "owner()" --rpc-url + # Should return 0x0000000000000000000000000000000000000000 + ``` + +### Step 6: Deploy FeeVault with Proxy as Owner + +When deploying FeeVault (post-genesis), use the AdminProxy address as the owner: + +```bash +OWNER=0x000000000000000000000000000000000000Ad00 \ +forge script script/DeployFeeVault.s.sol --broadcast --rpc-url +``` + +Alternatively, if deploying FeeVault at genesis too, add it to the alloc section with its storage slot 0 (owner) set to the proxy address: + +```json +{ + "alloc": { + "000000000000000000000000000000000000Ad00": { + "balance": "0x0", + "code": "0x", + "storage": {} + }, + "": { + "balance": "0x0", + "code": "0x", + "storage": { + "0x0": "0x000000000000000000000000000000000000000000000000000000000000Ad00" + } + } + } +} +``` + +Note: FeeVault has additional storage slots that need to be set. See `docs/contracts/fee_vault.md` for details. + +## Post-Genesis Setup + +### 1. Claim Ownership + +An authorized EOA claims initial ownership: + +```solidity +AdminProxy proxy = AdminProxy(0x000000000000000000000000000000000000Ad00); +proxy.claimOwnership(); // First caller becomes owner +``` + +### 2. Deploy Multisig + +Deploy your multisig (e.g., Safe) through normal transaction flow. + +### 3. Transfer Ownership + +Two-step transfer to multisig for safety: + +```solidity +// Step 1: Current owner initiates transfer +proxy.transferOwnership(multisigAddress); + +// Step 2: Multisig accepts (requires multisig transaction) +proxy.acceptOwnership(); // Called by multisig +``` + +## Usage Examples + +### Managing Mint Precompile Allowlist + +```solidity +AdminProxy proxy = AdminProxy(ADMIN_PROXY_ADDRESS); + +// Add address to allowlist +proxy.execute( + MINT_PRECOMPILE, + abi.encodeWithSignature("addToAllowList(address)", userAddress) +); + +// Remove from allowlist +proxy.execute( + MINT_PRECOMPILE, + abi.encodeWithSignature("removeFromAllowList(address)", userAddress) +); + +// Batch add multiple addresses +address[] memory targets = new address[](3); +bytes[] memory datas = new bytes[](3); +targets[0] = targets[1] = targets[2] = MINT_PRECOMPILE; +datas[0] = abi.encodeWithSignature("addToAllowList(address)", user1); +datas[1] = abi.encodeWithSignature("addToAllowList(address)", user2); +datas[2] = abi.encodeWithSignature("addToAllowList(address)", user3); +proxy.executeBatch(targets, datas); +``` + +### Managing FeeVault + +```solidity +AdminProxy proxy = AdminProxy(ADMIN_PROXY_ADDRESS); +FeeVault vault = FeeVault(FEE_VAULT_ADDRESS); + +// Update minimum amount +proxy.execute( + address(vault), + abi.encodeWithSignature("setMinimumAmount(uint256)", 2 ether) +); + +// Update bridge share +proxy.execute( + address(vault), + abi.encodeWithSignature("setBridgeShare(uint256)", 8000) // 80% +); +``` + +## Security Considerations + +### Two-Step Ownership Transfer + +The proxy uses a two-step transfer pattern (`transferOwnership` + `acceptOwnership`) to prevent accidental transfers to wrong addresses. The pending owner must explicitly accept. + +### Cancel Transfer + +If a transfer was initiated to the wrong address, the current owner can cancel: + +```solidity +proxy.cancelTransfer(); +``` + +### Zero Owner Bootstrap + +The contract initializes with `owner = address(0)`. This allows `claimOwnership()` to be called by the first authorized party post-genesis. Once claimed, this path is closed. + +### Call Forwarding + +The `execute` function forwards calls with the proxy as `msg.sender`. Target contracts see the proxy as the caller, not the original sender. This is intentional for the admin pattern. + +## Contract Interface + +| Function | Description | Access | +|----------|-------------|--------| +| `claimOwnership()` | Claim ownership when owner is zero | Anyone (once) | +| `transferOwnership(address)` | Start two-step transfer | Owner | +| `acceptOwnership()` | Complete two-step transfer | Pending owner | +| `cancelTransfer()` | Cancel pending transfer | Owner | +| `execute(address, bytes)` | Forward single call | Owner | +| `executeBatch(address[], bytes[])` | Forward multiple calls | Owner | +| `executeWithValue(address, bytes, uint256)` | Forward call with ETH | Owner | + +## Events + +| Event | Description | +|-------|-------------| +| `OwnershipTransferStarted(address, address)` | Transfer initiated | +| `OwnershipTransferred(address, address)` | Transfer completed | +| `Executed(address, bytes, bytes)` | Call forwarded | + +## Recommended Address + +We suggest deploying AdminProxy at `0x000000000000000000000000000000000000Ad00` for easy identification. The `Ad` prefix suggests "Admin". From 694723325a9a54cc644907bab64e2332ca32a32f Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 8 Jan 2026 17:50:31 +0100 Subject: [PATCH 3/5] remove claim ownership --- .../script/GenerateAdminProxyAlloc.s.sol | 41 ++++-- contracts/src/AdminProxy.sol | 29 ++-- contracts/test/AdminProxy.t.sol | 136 +++++++++++------- docs/contracts/admin_proxy.md | 107 ++++++++++---- 4 files changed, 208 insertions(+), 105 deletions(-) diff --git a/contracts/script/GenerateAdminProxyAlloc.s.sol b/contracts/script/GenerateAdminProxyAlloc.s.sol index 89742fc..2e7e8ba 100644 --- a/contracts/script/GenerateAdminProxyAlloc.s.sol +++ b/contracts/script/GenerateAdminProxyAlloc.s.sol @@ -6,32 +6,46 @@ import {AdminProxy} from "../src/AdminProxy.sol"; /// @title GenerateAdminProxyAlloc /// @notice Generates genesis alloc JSON for deploying AdminProxy at a deterministic address -/// @dev Run with: forge script script/GenerateAdminProxyAlloc.s.sol -vvv +/// @dev Run with: OWNER=0xYourAddress forge script script/GenerateAdminProxyAlloc.s.sol -vvv /// /// This script outputs the bytecode and storage layout needed to deploy AdminProxy -/// in the genesis block. The contract is deployed with owner = address(0), allowing -/// the first caller to claim ownership post-genesis. +/// in the genesis block. The owner is set directly in storage slot 0. /// /// Usage: -/// 1. Run this script to get the bytecode -/// 2. Add to genesis.json alloc section at desired address (e.g., 0x...AD00) -/// 3. Set that address as mintAdmin in chainspec config +/// 1. Set OWNER env var to your initial admin EOA address +/// 2. Run this script to get the bytecode and storage +/// 3. Add to genesis.json alloc section at desired address (e.g., 0x...Ad00) +/// 4. Set that address as mintAdmin in chainspec config contract GenerateAdminProxyAlloc is Script { // Suggested deterministic address for AdminProxy // Using a memorable address in the precompile-adjacent range address constant SUGGESTED_ADDRESS = 0x000000000000000000000000000000000000Ad00; function run() external { + // Get owner from environment, default to zero if not set + address owner = vm.envOr("OWNER", address(0)); + // Deploy to get runtime bytecode AdminProxy proxy = new AdminProxy(); // Get runtime bytecode (not creation code) bytes memory runtimeCode = address(proxy).code; + // Convert owner to storage slot value (left-padded to 32 bytes) + bytes32 ownerSlotValue = bytes32(uint256(uint160(owner))); + console.log("========== AdminProxy Genesis Alloc =========="); console.log(""); console.log("Suggested address:", SUGGESTED_ADDRESS); + console.log("Owner (from OWNER env):", owner); console.log(""); + + if (owner == address(0)) { + console.log("WARNING: OWNER not set! Set OWNER env var to your admin EOA."); + console.log("Example: OWNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 forge script ..."); + console.log(""); + } + console.log("Add this to your genesis.json 'alloc' section:"); console.log(""); console.log("{"); @@ -39,7 +53,9 @@ contract GenerateAdminProxyAlloc is Script { console.log(' "000000000000000000000000000000000000Ad00": {'); console.log(' "balance": "0x0",'); console.log(' "code": "0x%s",', vm.toString(runtimeCode)); - console.log(' "storage": {}'); + console.log(' "storage": {'); + console.log(' "0x0": "0x%s"', vm.toString(ownerSlotValue)); + console.log(" }"); console.log(" }"); console.log(" }"); console.log("}"); @@ -58,7 +74,7 @@ contract GenerateAdminProxyAlloc is Script { console.log("=============================================="); console.log(""); console.log("Post-genesis steps:"); - console.log("1. Call claimOwnership() from desired EOA"); + console.log("1. Owner can immediately use the proxy (no claiming needed)"); console.log("2. Deploy multisig (e.g., Safe)"); console.log("3. Call transferOwnership(multisigAddress)"); console.log("4. From multisig, call acceptOwnership()"); @@ -66,22 +82,29 @@ contract GenerateAdminProxyAlloc is Script { // Also output raw values for programmatic use console.log("Raw bytecode length:", runtimeCode.length); + console.log("Owner storage slot (0x0):", vm.toString(ownerSlotValue)); } } /// @title GenerateAdminProxyAllocJSON /// @notice Outputs just the JSON snippet for easy copy-paste +/// @dev Run with: OWNER=0xYourAddress forge script script/GenerateAdminProxyAlloc.s.sol:GenerateAdminProxyAllocJSON -vvv contract GenerateAdminProxyAllocJSON is Script { function run() external { + address owner = vm.envOr("OWNER", address(0)); + AdminProxy proxy = new AdminProxy(); bytes memory runtimeCode = address(proxy).code; + bytes32 ownerSlotValue = bytes32(uint256(uint160(owner))); // Output minimal JSON that can be merged into genesis string memory json = string( abi.encodePacked( '{"000000000000000000000000000000000000Ad00":{"balance":"0x0","code":"0x', vm.toString(runtimeCode), - '","storage":{}}}' + '","storage":{"0x0":"0x', + vm.toString(ownerSlotValue), + '"}}}' ) ); diff --git a/contracts/src/AdminProxy.sol b/contracts/src/AdminProxy.sol index 1961de8..d64f896 100644 --- a/contracts/src/AdminProxy.sol +++ b/contracts/src/AdminProxy.sol @@ -3,18 +3,22 @@ pragma solidity ^0.8.24; /// @title AdminProxy /// @notice A proxy contract for managing admin rights to precompiles and other contracts. -/// @dev Deployed at genesis with zero owner, allowing first-come claim. Supports two-step +/// @dev Deployed at genesis with owner set via storage slot. Supports two-step /// ownership transfer for safe handoff to multisigs or other governance contracts. /// /// This contract solves the bootstrap problem where admin addresses (e.g., multisigs) /// are not known at genesis time. The proxy is set as admin in the chainspec, and -/// ownership can be claimed and transferred post-genesis. +/// an initial EOA owner is set in genesis storage. Post-genesis, ownership can be +/// transferred to a multisig. +/// +/// Storage Layout: +/// - Slot 0: owner (address) +/// - Slot 1: pendingOwner (address) /// /// Usage: -/// 1. Deploy at genesis with zero owner (via genesis alloc) +/// 1. Deploy at genesis via alloc with owner set in storage slot 0 /// 2. Set proxy address as `mintAdmin` in chainspec and as FeeVault owner -/// 3. Post-genesis: call claimOwnership() to become initial owner -/// 4. Deploy multisig, then transferOwnership() -> acceptOwnership() to hand off +/// 3. Post-genesis: deploy multisig, then transferOwnership() -> acceptOwnership() contract AdminProxy { /// @notice Current owner of the proxy address public owner; @@ -51,18 +55,9 @@ contract AdminProxy { _; } - /// @notice Initialize with zero owner - first caller can claim ownership - constructor() { - owner = address(0); - } - - /// @notice Claim ownership when owner is zero (genesis bootstrap) - /// @dev Can only be called once, when owner is address(0) - function claimOwnership() external { - if (owner != address(0)) revert NotOwner(); - owner = msg.sender; - emit OwnershipTransferred(address(0), msg.sender); - } + /// @notice Constructor is empty - owner is set via genesis storage slot 0 + /// @dev When deploying at genesis, set storage["0x0"] to the owner address + constructor() {} /// @notice Start two-step ownership transfer /// @param newOwner Address of the new owner (e.g., multisig) diff --git a/contracts/test/AdminProxy.t.sol b/contracts/test/AdminProxy.t.sol index bf2a9be..4404e4e 100644 --- a/contracts/test/AdminProxy.t.sol +++ b/contracts/test/AdminProxy.t.sol @@ -68,6 +68,9 @@ contract AdminProxyTest is Test { address public bob = address(0x2); address public multisig = address(0x3); + // Storage slot for owner (slot 0) + bytes32 constant OWNER_SLOT = bytes32(uint256(0)); + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event Executed(address indexed target, bytes data, bytes result); @@ -77,34 +80,26 @@ contract AdminProxyTest is Test { target = new MockTarget(); } + /// @dev Helper to set owner via storage (simulating genesis) + function _setOwnerViaStorage(address _owner) internal { + vm.store(address(proxy), OWNER_SLOT, bytes32(uint256(uint160(_owner)))); + } + // ============ Ownership Tests ============ function test_InitialOwnerIsZero() public view { assertEq(proxy.owner(), address(0)); } - function test_ClaimOwnership() public { - vm.prank(alice); - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(address(0), alice); - proxy.claimOwnership(); - + function test_OwnerSetViaStorage() public { + // Simulate genesis by setting owner in storage + _setOwnerViaStorage(alice); assertEq(proxy.owner(), alice); } - function test_ClaimOwnership_RevertWhenAlreadyClaimed() public { - vm.prank(alice); - proxy.claimOwnership(); - - vm.prank(bob); - vm.expectRevert(AdminProxy.NotOwner.selector); - proxy.claimOwnership(); - } - function test_TransferOwnership_TwoStep() public { - // Alice claims ownership - vm.prank(alice); - proxy.claimOwnership(); + // Set alice as owner via storage (genesis simulation) + _setOwnerViaStorage(alice); // Alice initiates transfer to bob vm.prank(alice); @@ -126,8 +121,7 @@ contract AdminProxyTest is Test { } function test_TransferOwnership_RevertZeroAddress() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); vm.prank(alice); vm.expectRevert(AdminProxy.ZeroAddress.selector); @@ -135,8 +129,7 @@ contract AdminProxyTest is Test { } function test_AcceptOwnership_RevertNotPending() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); vm.prank(alice); proxy.transferOwnership(bob); @@ -149,8 +142,7 @@ contract AdminProxyTest is Test { } function test_CancelTransfer() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); vm.prank(alice); proxy.transferOwnership(bob); @@ -167,19 +159,31 @@ contract AdminProxyTest is Test { } function test_TransferOwnership_RevertNotOwner() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); vm.prank(bob); vm.expectRevert(AdminProxy.NotOwner.selector); proxy.transferOwnership(bob); } + function test_OwnerZero_CannotCallOwnerFunctions() public { + // Owner is zero (not set) + assertEq(proxy.owner(), address(0)); + + // Nobody can call owner functions + vm.prank(alice); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.transferOwnership(alice); + + vm.prank(alice); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.execute(address(target), abi.encodeCall(MockTarget.setValue, (42))); + } + // ============ Execute Tests ============ function test_Execute() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); bytes memory data = abi.encodeCall(MockTarget.setValue, (42)); @@ -193,8 +197,7 @@ contract AdminProxyTest is Test { } function test_Execute_ReturnsData() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); // First set a value vm.prank(alice); @@ -209,8 +212,7 @@ contract AdminProxyTest is Test { } function test_Execute_RevertNotOwner() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); vm.prank(bob); vm.expectRevert(AdminProxy.NotOwner.selector); @@ -218,8 +220,7 @@ contract AdminProxyTest is Test { } function test_Execute_PropagatesRevert() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); // The revert data is ABI-encoded as Error(string), not raw bytes bytes memory expectedRevertData = abi.encodeWithSignature("Error(string)", "MockTarget: intentional revert"); @@ -232,8 +233,7 @@ contract AdminProxyTest is Test { // ============ ExecuteBatch Tests ============ function test_ExecuteBatch() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); MockTarget target2 = new MockTarget(); @@ -253,8 +253,7 @@ contract AdminProxyTest is Test { } function test_ExecuteBatch_RevertLengthMismatch() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); address[] memory targets = new address[](2); bytes[] memory datas = new bytes[](1); @@ -265,8 +264,7 @@ contract AdminProxyTest is Test { } function test_ExecuteBatch_RevertOnAnyFailure() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); address[] memory targets = new address[](2); targets[0] = address(target); @@ -287,8 +285,7 @@ contract AdminProxyTest is Test { // ============ ExecuteWithValue Tests ============ function test_ExecuteWithValue() public { - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); // Fund the proxy vm.deal(address(proxy), 1 ether); @@ -312,9 +309,8 @@ contract AdminProxyTest is Test { // Deploy mint precompile with proxy as admin mintPrecompile = new MockMintPrecompile(address(proxy)); - // Alice claims proxy ownership - vm.prank(alice); - proxy.claimOwnership(); + // Set alice as owner via storage (simulating genesis) + _setOwnerViaStorage(alice); // Alice uses proxy to add bob to allowlist vm.prank(alice); @@ -332,9 +328,8 @@ contract AdminProxyTest is Test { // Simulate genesis -> multisig flow mintPrecompile = new MockMintPrecompile(address(proxy)); - // 1. Alice (EOA) claims ownership post-genesis - vm.prank(alice); - proxy.claimOwnership(); + // 1. Alice is set as owner at genesis (via storage) + _setOwnerViaStorage(alice); // 2. Alice does some admin work vm.prank(alice); @@ -375,9 +370,8 @@ contract AdminProxyTest is Test { address(0x99) ); - // Alice claims proxy ownership - vm.prank(alice); - proxy.claimOwnership(); + // Set alice as owner via storage (simulating genesis) + _setOwnerViaStorage(alice); // Alice uses proxy to update FeeVault config vm.prank(alice); @@ -394,8 +388,7 @@ contract AdminProxyTest is Test { function test_Integration_BatchAllowlistUpdates() public { mintPrecompile = new MockMintPrecompile(address(proxy)); - vm.prank(alice); - proxy.claimOwnership(); + _setOwnerViaStorage(alice); // Batch add multiple addresses to allowlist address[] memory targets = new address[](3); @@ -420,4 +413,41 @@ contract AdminProxyTest is Test { assertTrue(mintPrecompile.allowlist(user2)); assertTrue(mintPrecompile.allowlist(user3)); } + + // ============ Genesis Simulation Tests ============ + + function test_GenesisSimulation_FullFlow() public { + // This test simulates exactly what happens at genesis and post-genesis + + // 1. At genesis: proxy is deployed at a specific address with owner set in storage + // We simulate this by deploying and then setting storage + AdminProxy genesisProxy = new AdminProxy(); + + // Set owner to alice (EOA) at genesis via storage slot 0 + address genesisOwner = address(0xAAAA); + vm.store(address(genesisProxy), OWNER_SLOT, bytes32(uint256(uint160(genesisOwner)))); + + // Verify owner was set + assertEq(genesisProxy.owner(), genesisOwner); + assertEq(genesisProxy.pendingOwner(), address(0)); + + // 2. Post-genesis: owner can immediately use the proxy + MockMintPrecompile precompile = new MockMintPrecompile(address(genesisProxy)); + + vm.prank(genesisOwner); + genesisProxy.execute(address(precompile), abi.encodeCall(MockMintPrecompile.addToAllowList, (address(0xBBBB)))); + + assertTrue(precompile.allowlist(address(0xBBBB))); + + // 3. Later: transfer to multisig + address multisigAddr = address(0xCCCC); + + vm.prank(genesisOwner); + genesisProxy.transferOwnership(multisigAddr); + + vm.prank(multisigAddr); + genesisProxy.acceptOwnership(); + + assertEq(genesisProxy.owner(), multisigAddr); + } } diff --git a/docs/contracts/admin_proxy.md b/docs/contracts/admin_proxy.md index adabafc..fb9c244 100644 --- a/docs/contracts/admin_proxy.md +++ b/docs/contracts/admin_proxy.md @@ -15,11 +15,13 @@ The challenge: these admin addresses often need to be multisigs (like Safe) for ## Solution -Deploy `AdminProxy` at genesis with `owner = address(0)`. Post-genesis: +Deploy `AdminProxy` at genesis with `owner` set directly in storage slot 0. This eliminates any race condition and ensures the designated admin has control from block 0. -1. An EOA claims ownership -2. The multisig is deployed -3. Ownership is transferred to the multisig via two-step transfer +Post-genesis: + +1. The owner (set at genesis) can immediately use the proxy +2. When ready, deploy the multisig +3. Transfer ownership to multisig via two-step transfer (`transferOwnership` + `acceptOwnership`) The proxy then forwards admin calls to the underlying contracts/precompiles. @@ -59,17 +61,17 @@ cd contracts forge build ``` -### Step 2: Get the Runtime Bytecode +### Step 2: Generate the Genesis Alloc Entry -There are two ways to get the bytecode: +**Option A: Use the helper script (recommended)** -**Option A: Use the helper script** +Set the `OWNER` environment variable to your initial admin EOA address: ```bash -forge script script/GenerateAdminProxyAlloc.s.sol -vvv +OWNER=0xYourEOAAddress forge script script/GenerateAdminProxyAlloc.s.sol -vvv ``` -This outputs the complete alloc entry you can copy into your genesis file. +This outputs the complete alloc entry with bytecode and storage, ready to copy into your genesis file. **Option B: Get bytecode directly from artifacts** @@ -80,33 +82,55 @@ After building, the runtime bytecode is in the compiled artifacts: cat out/AdminProxy.sol/AdminProxy.json | jq -r '.deployedBytecode.object' ``` -This outputs the hex string starting with `0x608060...`. +This outputs the hex string starting with `0x608060...`. You'll need to manually construct the storage entry for the owner (see Step 3). ### Step 3: Create the Genesis Alloc Entry -The genesis `alloc` section pre-deploys contracts at specific addresses. For AdminProxy: +The genesis `alloc` section pre-deploys contracts at specific addresses. For AdminProxy, you must set the owner in storage slot 0. + +**Storage Layout:** + +| Slot | Variable | Type | +|------|----------|------| +| 0 | `owner` | `address` | +| 1 | `pendingOwner` | `address` | + +**Converting owner address to storage value:** + +The owner address must be left-padded to 32 bytes. For example, if your owner EOA is `0x1234567890abcdef1234567890abcdef12345678`: + +``` +Storage slot 0x0 = 0x0000000000000000000000001234567890abcdef1234567890abcdef12345678 +``` + +**Example alloc entry:** ```json { "alloc": { "000000000000000000000000000000000000Ad00": { "balance": "0x0", - "code": "0x608060405234801561001057600080fd5b50600436106100935760003560e01c80638da5cb5b116100665780638da5cb5b146100fa578063b0e21e8a1461010b578063e30c39781461011e578063f2fde38b1461012f578063fe0d94c11461014257600080fd5b80631a69523014610098578063715018a6146100ad57806374e6310e146100b557806379ba5097146100f2575b600080fd5b6100ab6100a63660046108d5565b610155565b005b6100ab6101d5565b6100c86100c33660046108f7565b6101e9565b604080516001600160a01b0390931683526020830191909152015b60405180910390f35b6100ab610283565b6000546001600160a01b03166100c8565b6100ab610119366004610938565b6102f8565b6001546001600160a01b03166100c8565b6100ab61013d3660046108d5565b610410565b6100ab6101503660046109ab565b610481565b61015d610528565b6001600160a01b03811661018457604051631e4fbdf760e01b815260040160405180910390fd5b600180546001600160a01b0319166001600160a01b0383169081179091556000546040516001600160a01b03909116907f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270090600090a350565b6101dd610528565b6101e76000610555565b565b60008061021d846040518060400160405280600e81526020016d2737ba1030b63637bbb2b2103a3960911b8152506105a5565b9050600080856001600160a01b031683866040516102449291906001600160a01b03929092168252602082015260400190565b6000604051808303816000875af1925050503d8060008114610282576040519150601f19603f3d011682016040523d82523d6000602084013e610287565b606091505b5091509150816102ac576102ae604051806060016040528060228152602001610b2f60229139836105d1565b505b6040805180820182526001600160a01b038089168252602080830188905283518581529182018690528451928301939093529051909116907f6e9b6e3f1f8e21e9d5e8f5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e8e5e89181900360600190a25090925050509250929050565b6001546001600160a01b031633146102e85760405163118cdaa760e01b81523360048201526024015b60405180910390fd5b6001805460006001600160a01b0319918216178255805482166001600160a01b03831690811782556040519192909116907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908490a350565b610300610528565b82811461032057604051634ec4810560e11b815260040160405180910390fd5b60005b838110156104095760008086868481811061034057610340610a4d565b90506020020160208101906103559190610938565b6001600160a01b031685858581811061037057610370610a4d565b905060200281019061038291906109f4565b604051610390929190610a3d565b6000604051808303816000865af19150503d80600081146103cd576040519150601f19603f3d011682016040523d82523d6000602084013e6103d2565b606091505b5091509150816103ff576103f9604051806060016040528060228152602001610b2f60229139836105d1565b50610400565b5b50600101610323565b5050505050565b610418610528565b6001600160a01b03811661043f57604051631e4fbdf760e01b815260040160405180910390fd5b600180546001600160a01b0319166001600160a01b0383169081179091556000546040516001600160a01b03909116907f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270090600090a350565b610489610528565b6000826001600160a01b031682846040516104a49190610a63565b60006040518083038185875af1925050503d80600081146104e1576040519150601f19603f3d011682016040523d82523d6000602084013e6104e6565b606091505b505090508061052257610522604051806060016040528060228152602001610b2f6022913960405180602001604052806000815250905090506105d1565b50505050565b6000546001600160a01b031633146101e75760405163118cdaa760e01b81523360048201526024016102df565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6060828260405160200161057b92919091825260601b6bffffffffffffffffffffffff1916602082015260340190565b604051602081830303815290604052905092915050565b60606105e183836040518060400160405280601e81526020017f416464726573733a206c6f772d6c6576656c2063616c6c206661696c65640000815250610648565b9392505050565b81516000036105f8575050565b8060000361060557505050565b815160208301fd5b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff811182821017156106485761064861060d565b604052919050565b919050565b600067ffffffffffffffff8211156106775761067761060d565b50601f01601f191660200190565b600082601f83011261069657600080fd5b81356106a96106a48261065d565b610623565b8181528460208386010111156106be57600080fd5b816020850160208301376000918101602001919091529392505050565b6000806000606084860312156106f057600080fd5b83356001600160a01b038116811461070757600080fd5b925060208401359150604084013567ffffffffffffffff81111561072a57600080fd5b61073686828701610685565b9150509250925092565b60008083601f84011261075257600080fd5b50813567ffffffffffffffff81111561076a57600080fd5b6020830191508360208260051b850101111561078557600080fd5b9250929050565b600080600080604085870312156107a257600080fd5b843567ffffffffffffffff808211156107ba57600080fd5b6107c688838901610740565b909650945060208701359150808211156107df57600080fd5b506107ec87828801610740565b95989497509550505050565b60005b838110156108135781810151838201526020016107fb565b50506000910152565b600081518084526108348160208601602086016107f8565b601f01601f19169290920160200192915050565b6001600160a01b038716815260208101869052604081018590526060810184905260c06080820181905260009061088190830185610823565b82810360a08401526108938185610823565b9998505050505050505050565b6000602082840312156108b257600080fd5b5035919050565b6001600160a01b03811681146108ce57600080fd5b50565b6000602082840312156108e357600080fd5b81356108ee816108b9565b9392505050565b6000806040838503121561090857600080fd5b8235610913816108b9565b9150602083013567ffffffffffffffff81111561092f57600080fd5b61093b85828601610685565b9150509250929050565b6000806000806040858703121561095b57600080fd5b843567ffffffffffffffff8082111561097357600080fd5b61097f88838901610740565b9650602087013591508082111561099557600080fd5b506107ec87828801610740565b634e487b7160e01b600052603260045260246000fd5b6000602082840312156109c957600080fd5b81356001600160a01b03811681146108ee57600080fd5b8183823760009101908152919050565b600082516109ff8184602087016107f8565b9190910192915050565b60008151808452610a218160208601602086016107f8565b601f01601f19169290920160200192915050565b6020815260006105e16020830184610a0956fe416464726573733a2063616c6c206661696c656420776974686f757420726576657274696e67a2646970667358221220...", - "storage": {} + "code": "0x", + "storage": { + "0x0": "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678" + } } } } ``` -**Important notes about the alloc entry:** +**Important notes:** 1. **Address format**: The address key does NOT have the `0x` prefix in the alloc section 2. **Code format**: The code value MUST have the `0x` prefix -3. **Storage**: Empty `{}` because AdminProxy initializes `owner = address(0)` and `pendingOwner = address(0)`, which are the default zero values (no explicit storage needed) +3. **Storage key**: Must be `"0x0"` (slot 0 for owner) +4. **Storage value**: Owner address left-padded to 32 bytes with `0x` prefix ### Step 4: Complete Genesis File Example -Here's a complete example showing how AdminProxy fits into the full genesis file: +Here's a complete example showing how AdminProxy fits into the full genesis file. + +In this example, the owner EOA is `0xYourEOAAddressHere` (replace with your actual address): ```json { @@ -142,12 +166,24 @@ Here's a complete example showing how AdminProxy fits into the full genesis file "000000000000000000000000000000000000Ad00": { "balance": "0x0", "code": "0x", - "storage": {} + "storage": { + "0x0": "0x000000000000000000000000" + } } } } ``` +**Example with concrete addresses:** + +If your owner EOA is `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`: + +```json +"storage": { + "0x0": "0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +} +``` + ### Step 5: Verify the Setup After creating your genesis file, you can verify the AdminProxy is correctly configured: @@ -200,13 +236,15 @@ Note: FeeVault has additional storage slots that need to be set. See `docs/contr ## Post-Genesis Setup -### 1. Claim Ownership +Since the owner is set at genesis, no claiming is required. The designated EOA can immediately use the proxy. -An authorized EOA claims initial ownership: +### 1. Verify Ownership -```solidity -AdminProxy proxy = AdminProxy(0x000000000000000000000000000000000000Ad00); -proxy.claimOwnership(); // First caller becomes owner +Confirm the owner was set correctly: + +```bash +cast call 0x000000000000000000000000000000000000Ad00 "owner()" --rpc-url +# Should return your EOA address ``` ### 2. Deploy Multisig @@ -218,11 +256,27 @@ Deploy your multisig (e.g., Safe) through normal transaction flow. Two-step transfer to multisig for safety: ```solidity +AdminProxy proxy = AdminProxy(0x000000000000000000000000000000000000Ad00); + // Step 1: Current owner initiates transfer proxy.transferOwnership(multisigAddress); // Step 2: Multisig accepts (requires multisig transaction) -proxy.acceptOwnership(); // Called by multisig +// This must be called FROM the multisig +proxy.acceptOwnership(); +``` + +Using cast: + +```bash +# Step 1: Owner initiates transfer +cast send 0x000000000000000000000000000000000000Ad00 \ + "transferOwnership(address)" \ + --private-key \ + --rpc-url + +# Step 2: Multisig accepts (execute via multisig UI/CLI) +# The multisig must call: acceptOwnership() ``` ## Usage Examples @@ -287,9 +341,9 @@ If a transfer was initiated to the wrong address, the current owner can cancel: proxy.cancelTransfer(); ``` -### Zero Owner Bootstrap +### Genesis Storage Initialization -The contract initializes with `owner = address(0)`. This allows `claimOwnership()` to be called by the first authorized party post-genesis. Once claimed, this path is closed. +The owner is set directly in storage slot 0 at genesis. This eliminates race conditions and ensures the designated admin has control from block 0. No `claimOwnership()` function exists, so there's no risk of front-running. ### Call Forwarding @@ -299,7 +353,8 @@ The `execute` function forwards calls with the proxy as `msg.sender`. Target con | Function | Description | Access | |----------|-------------|--------| -| `claimOwnership()` | Claim ownership when owner is zero | Anyone (once) | +| `owner()` | Current owner address | View | +| `pendingOwner()` | Pending owner for two-step transfer | View | | `transferOwnership(address)` | Start two-step transfer | Owner | | `acceptOwnership()` | Complete two-step transfer | Pending owner | | `cancelTransfer()` | Cancel pending transfer | Owner | From 031c94fe0e892e3e820e5bc447e9bdfb94fa1a8c Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 8 Jan 2026 18:36:23 +0100 Subject: [PATCH 4/5] lint --- contracts/script/GenerateAdminProxyAlloc.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/script/GenerateAdminProxyAlloc.s.sol b/contracts/script/GenerateAdminProxyAlloc.s.sol index 2e7e8ba..423a209 100644 --- a/contracts/script/GenerateAdminProxyAlloc.s.sol +++ b/contracts/script/GenerateAdminProxyAlloc.s.sol @@ -62,7 +62,7 @@ contract GenerateAdminProxyAlloc is Script { console.log(""); console.log("Then update chainspec config:"); console.log(""); - console.log('{'); + console.log("{"); console.log(' "config": {'); console.log(' "evolve": {'); console.log(' "mintAdmin": "0x000000000000000000000000000000000000Ad00",'); From 88190115f8abb84a4984640830efe30cbe76d952 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 9 Jan 2026 10:41:17 +0100 Subject: [PATCH 5/5] fixes --- contracts/README.md | 6 ++++++ crates/ev-precompiles/README.md | 10 ++++++++++ docs/contracts/admin_proxy.md | 9 +++++++-- etc/ev-reth-genesis.json | 15 +++++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index 9465e1c..4c6e0a3 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -2,6 +2,12 @@ Smart contracts for EV-Reth, including the FeeVault for bridging collected fees to Celestia. +## AdminProxy + +The AdminProxy contract solves the bootstrap problem for admin addresses at genesis. It acts as an intermediary owner/admin for other contracts and precompiles (like the Mint Precompile) when the final admin (e.g., a multisig) is not known at genesis time. + +See [AdminProxy documentation](../docs/contracts/admin_proxy.md) for detailed setup and usage instructions. + ## FeeVault The FeeVault contract collects base fees and bridges them to Celestia via Hyperlane. It supports: diff --git a/crates/ev-precompiles/README.md b/crates/ev-precompiles/README.md index 89a304a..bf8dbb8 100644 --- a/crates/ev-precompiles/README.md +++ b/crates/ev-precompiles/README.md @@ -58,10 +58,12 @@ Calls from any other address will be rejected with an "unauthorized caller" erro Mints new native tokens to a specified address. **Parameters:** + - `to` (address): Recipient address - `amount` (uint256): Amount to mint in wei **Behavior:** + 1. Verifies caller is the authorized mint admin 2. Creates the recipient account if it doesn't exist 3. Increases the recipient's balance by the specified amount @@ -70,6 +72,7 @@ Mints new native tokens to a specified address. **Gas:** Returns unused gas (precompile consumes minimal gas) **Errors:** + - `unauthorized caller`: Caller is not the mint admin - `balance overflow`: Adding the amount would overflow uint256 @@ -78,10 +81,12 @@ Mints new native tokens to a specified address. Burns native tokens from a specified address. **Parameters:** + - `from` (address): Address to burn tokens from - `amount` (uint256): Amount to burn in wei **Behavior:** + 1. Verifies caller is the authorized mint admin 2. Ensures the target account exists 3. Decreases the target's balance by the specified amount @@ -90,6 +95,7 @@ Burns native tokens from a specified address. **Gas:** Returns unused gas (precompile consumes minimal gas) **Errors:** + - `unauthorized caller`: Caller is not the mint admin - `insufficient balance`: Account doesn't have enough balance to burn @@ -99,11 +105,14 @@ The typical usage pattern involves deploying a proxy contract at the mint admin This pattern allows the mint admin to be a smart contract with custom authorization logic (multisig, governance, etc.) rather than a simple EOA. +See the [AdminProxy documentation](../../docs/contracts/admin_proxy.md) for a ready-to-use proxy contract that can be deployed at genesis and later upgraded to a multisig. + ## Implementation Details ### Account Creation The precompile automatically creates accounts that don't exist when minting to them. This ensures that: + - Tokens can be minted to any address, including those not yet active on-chain - The account is properly marked as created in the EVM state - The account is touched for accurate state tracking @@ -111,6 +120,7 @@ The precompile automatically creates accounts that don't exist when minting to t ### Balance Manipulation The precompile directly modifies account balances in the EVM state using the `EvmInternals` API. This provides: + - **Direct state access**: No need for complex transfer mechanisms - **Overflow protection**: All arithmetic is checked - **State consistency**: Accounts are properly touched for journaling diff --git a/docs/contracts/admin_proxy.md b/docs/contracts/admin_proxy.md index fb9c244..b9ae946 100644 --- a/docs/contracts/admin_proxy.md +++ b/docs/contracts/admin_proxy.md @@ -169,11 +169,16 @@ In this example, the owner EOA is `0xYourEOAAddressHere` (replace with your actu "storage": { "0x0": "0x000000000000000000000000" } + }, + "": { + "balance": "0x56bc75e2d63100000" } } } ``` +**Note:** The owner EOA must also be funded with gas at genesis to execute transactions. In the example above, `0x56bc75e2d63100000` equals 100 ETH in wei. + **Example with concrete addresses:** If your owner EOA is `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`: @@ -195,11 +200,11 @@ After creating your genesis file, you can verify the AdminProxy is correctly con cast code 0x000000000000000000000000000000000000Ad00 --rpc-url ``` -3. **Check owner is zero** (ready for claiming): +3. **Verify owner is set correctly**: ```bash cast call 0x000000000000000000000000000000000000Ad00 "owner()" --rpc-url - # Should return 0x0000000000000000000000000000000000000000 + # Should return your EOA address (e.g., 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) ``` ### Step 6: Deploy FeeVault with Proxy as Owner diff --git a/etc/ev-reth-genesis.json b/etc/ev-reth-genesis.json index 69f4561..024e0c7 100644 --- a/etc/ev-reth-genesis.json +++ b/etc/ev-reth-genesis.json @@ -19,7 +19,7 @@ "evolve": { "baseFeeSink": "0x00000000000000000000000000000000000000fe", "baseFeeRedirectActivationHeight": 0, - "mintAdmin": "0x0000000000000000000000000000000000000000", + "mintAdmin": "0x000000000000000000000000000000000000Ad00", "mintPrecompileActivationHeight": 0, "contractSizeLimit": 131072, "contractSizeLimitActivationHeight": 0 @@ -27,5 +27,16 @@ }, "difficulty": "0x1", "gasLimit": "0x1c9c380", - "alloc": {} + "alloc": { + "000000000000000000000000000000000000Ad00": { + "balance": "0x0", + "code": "0x60806040526004361061007e575f3560e01c80638da5cb5b1161004d5780638da5cb5b1461012d578063e30c397814610157578063f2fde38b14610181578063fa4bb79d146101a957610085565b806318dfb3c7146100895780631cff79cd146100c557806379ba5097146101015780638b5298541461011757610085565b3661008557005b5f5ffd5b348015610094575f5ffd5b506100af60048036038101906100aa9190610cf8565b6101e5565b6040516100bc9190610ea1565b60405180910390f35b3480156100d0575f5ffd5b506100eb60048036038101906100e69190610f70565b6104d9565b6040516100f89190611015565b60405180910390f35b34801561010c575f5ffd5b5061011561066c565b005b348015610122575f5ffd5b5061012b6107ed565b005b348015610138575f5ffd5b506101416108b4565b60405161014e9190611044565b60405180910390f35b348015610162575f5ffd5b5061016b6108d8565b6040516101789190611044565b60405180910390f35b34801561018c575f5ffd5b506101a760048036038101906101a2919061105d565b6108fd565b005b3480156101b4575f5ffd5b506101cf60048036038101906101ca91906110bb565b610aa4565b6040516101dc9190611015565b60405180910390f35b60605f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461026c576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8282905085859050146102ab576040517fff633a3800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8484905067ffffffffffffffff8111156102c8576102c761112c565b5b6040519080825280602002602001820160405280156102fb57816020015b60608152602001906001900390816102e65790505b5090505f5f90505b858590508110156104d0575f5f87878481811061032357610322611159565b5b9050602002016020810190610338919061105d565b73ffffffffffffffffffffffffffffffffffffffff1686868581811061036157610360611159565b5b90506020028101906103739190611192565b604051610381929190611230565b5f604051808303815f865af19150503d805f81146103ba576040519150601f19603f3d011682016040523d82523d5f602084013e6103bf565b606091505b50915091508161040657806040517fa5fa8d2b0000000000000000000000000000000000000000000000000000000081526004016103fd9190611015565b60405180910390fd5b87878481811061041957610418611159565b5b905060200201602081019061042e919061105d565b73ffffffffffffffffffffffffffffffffffffffff167fc96720f35dd524e76ea92971ce13d08e9a17816bf3b0008a7083e6032354ebb587878681811061047857610477611159565b5b905060200281019061048a9190611192565b8460405161049a93929190611274565b60405180910390a2808484815181106104b6576104b5611159565b5b602002602001018190525050508080600101915050610303565b50949350505050565b60605f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610560576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5f8573ffffffffffffffffffffffffffffffffffffffff168585604051610589929190611230565b5f604051808303815f865af19150503d805f81146105c2576040519150601f19603f3d011682016040523d82523d5f602084013e6105c7565b606091505b50915091508161060e57806040517fa5fa8d2b0000000000000000000000000000000000000000000000000000000081526004016106059190611015565b60405180910390fd5b8573ffffffffffffffffffffffffffffffffffffffff167fc96720f35dd524e76ea92971ce13d08e9a17816bf3b0008a7083e6032354ebb586868460405161065893929190611274565b60405180910390a280925050509392505050565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146106f2576040517f1853971c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff165f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3335f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505f60015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610872576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f60015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610982576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16036109e7576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8060015f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508073ffffffffffffffffffffffffffffffffffffffff165f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b60605f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610b2b576040517f30cd747100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5f8673ffffffffffffffffffffffffffffffffffffffff16848787604051610b55929190611230565b5f6040518083038185875af1925050503d805f8114610b8f576040519150601f19603f3d011682016040523d82523d5f602084013e610b94565b606091505b509150915081610bdb57806040517fa5fa8d2b000000000000000000000000000000000000000000000000000000008152600401610bd29190611015565b60405180910390fd5b8673ffffffffffffffffffffffffffffffffffffffff167fc96720f35dd524e76ea92971ce13d08e9a17816bf3b0008a7083e6032354ebb5878784604051610c2593929190611274565b60405180910390a28092505050949350505050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f840112610c6357610c62610c42565b5b8235905067ffffffffffffffff811115610c8057610c7f610c46565b5b602083019150836020820283011115610c9c57610c9b610c4a565b5b9250929050565b5f5f83601f840112610cb857610cb7610c42565b5b8235905067ffffffffffffffff811115610cd557610cd4610c46565b5b602083019150836020820283011115610cf157610cf0610c4a565b5b9250929050565b5f5f5f5f60408587031215610d1057610d0f610c3a565b5b5f85013567ffffffffffffffff811115610d2d57610d2c610c3e565b5b610d3987828801610c4e565b9450945050602085013567ffffffffffffffff811115610d5c57610d5b610c3e565b5b610d6887828801610ca3565b925092505092959194509250565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f610de182610d9f565b610deb8185610da9565b9350610dfb818560208601610db9565b610e0481610dc7565b840191505092915050565b5f610e1a8383610dd7565b905092915050565b5f602082019050919050565b5f610e3882610d76565b610e428185610d80565b935083602082028501610e5485610d90565b805f5b85811015610e8f5784840389528151610e708582610e0f565b9450610e7b83610e22565b925060208a01995050600181019050610e57565b50829750879550505050505092915050565b5f6020820190508181035f830152610eb98184610e2e565b905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610eea82610ec1565b9050919050565b610efa81610ee0565b8114610f04575f5ffd5b50565b5f81359050610f1581610ef1565b92915050565b5f5f83601f840112610f3057610f2f610c42565b5b8235905067ffffffffffffffff811115610f4d57610f4c610c46565b5b602083019150836001820283011115610f6957610f68610c4a565b5b9250929050565b5f5f5f60408486031215610f8757610f86610c3a565b5b5f610f9486828701610f07565b935050602084013567ffffffffffffffff811115610fb557610fb4610c3e565b5b610fc186828701610f1b565b92509250509250925092565b5f82825260208201905092915050565b5f610fe782610d9f565b610ff18185610fcd565b9350611001818560208601610db9565b61100a81610dc7565b840191505092915050565b5f6020820190508181035f83015261102d8184610fdd565b905092915050565b61103e81610ee0565b82525050565b5f6020820190506110575f830184611035565b92915050565b5f6020828403121561107257611071610c3a565b5b5f61107f84828501610f07565b91505092915050565b5f819050919050565b61109a81611088565b81146110a4575f5ffd5b50565b5f813590506110b581611091565b92915050565b5f5f5f5f606085870312156110d3576110d2610c3a565b5b5f6110e087828801610f07565b945050602085013567ffffffffffffffff81111561110157611100610c3e565b5b61110d87828801610f1b565b93509350506040611120878288016110a7565b91505092959194509250565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f833560016020038436030381126111ae576111ad611186565b5b80840192508235915067ffffffffffffffff8211156111d0576111cf61118a565b5b6020830192506001820236038313156111ec576111eb61118e565b5b509250929050565b5f81905092915050565b828183375f83830152505050565b5f61121783856111f4565b93506112248385846111fe565b82840190509392505050565b5f61123c82848661120c565b91508190509392505050565b5f6112538385610fcd565b93506112608385846111fe565b61126983610dc7565b840190509392505050565b5f6040820190508181035f83015261128d818587611248565b905081810360208301526112a18184610fdd565b905094935050505056fea26469706673582212201029704c8e76cc8133cedd39a8adbebfe979b8809644c7f5e9cff417e23119d464736f6c634300081e0033", + "storage": { + "0x0": "0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + } + }, + "f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { + "balance": "0x56bc75e2d63100000" + } + } }