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/contracts/script/GenerateAdminProxyAlloc.s.sol b/contracts/script/GenerateAdminProxyAlloc.s.sol new file mode 100644 index 0000000..423a209 --- /dev/null +++ b/contracts/script/GenerateAdminProxyAlloc.s.sol @@ -0,0 +1,113 @@ +// 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: 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 owner is set directly in storage slot 0. +/// +/// Usage: +/// 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("{"); + console.log(' "alloc": {'); + console.log(' "000000000000000000000000000000000000Ad00": {'); + console.log(' "balance": "0x0",'); + console.log(' "code": "0x%s",', vm.toString(runtimeCode)); + console.log(' "storage": {'); + console.log(' "0x0": "0x%s"', vm.toString(ownerSlotValue)); + console.log(" }"); + 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. 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()"); + console.log(""); + + // 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":{"0x0":"0x', + vm.toString(ownerSlotValue), + '"}}}' + ) + ); + + console.log(json); + } +} diff --git a/contracts/src/AdminProxy.sol b/contracts/src/AdminProxy.sol new file mode 100644 index 0000000..d64f896 --- /dev/null +++ b/contracts/src/AdminProxy.sol @@ -0,0 +1,147 @@ +// 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 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 +/// 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 via alloc with owner set in storage slot 0 +/// 2. Set proxy address as `mintAdmin` in chainspec and as FeeVault owner +/// 3. Post-genesis: deploy multisig, then transferOwnership() -> acceptOwnership() +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 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) + 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..4404e4e --- /dev/null +++ b/contracts/test/AdminProxy.t.sol @@ -0,0 +1,453 @@ +// 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); + + // 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); + + function setUp() public { + proxy = new AdminProxy(); + 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_OwnerSetViaStorage() public { + // Simulate genesis by setting owner in storage + _setOwnerViaStorage(alice); + assertEq(proxy.owner(), alice); + } + + function test_TransferOwnership_TwoStep() public { + // Set alice as owner via storage (genesis simulation) + _setOwnerViaStorage(alice); + + // 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 { + _setOwnerViaStorage(alice); + + vm.prank(alice); + vm.expectRevert(AdminProxy.ZeroAddress.selector); + proxy.transferOwnership(address(0)); + } + + function test_AcceptOwnership_RevertNotPending() public { + _setOwnerViaStorage(alice); + + 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 { + _setOwnerViaStorage(alice); + + 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 { + _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 { + _setOwnerViaStorage(alice); + + 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 { + _setOwnerViaStorage(alice); + + // 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 { + _setOwnerViaStorage(alice); + + vm.prank(bob); + vm.expectRevert(AdminProxy.NotOwner.selector); + proxy.execute(address(target), abi.encodeCall(MockTarget.setValue, (42))); + } + + function test_Execute_PropagatesRevert() public { + _setOwnerViaStorage(alice); + + // 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 { + _setOwnerViaStorage(alice); + + 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 { + _setOwnerViaStorage(alice); + + 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 { + _setOwnerViaStorage(alice); + + 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 { + _setOwnerViaStorage(alice); + + // 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)); + + // Set alice as owner via storage (simulating genesis) + _setOwnerViaStorage(alice); + + // 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 is set as owner at genesis (via storage) + _setOwnerViaStorage(alice); + + // 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) + ); + + // Set alice as owner via storage (simulating genesis) + _setOwnerViaStorage(alice); + + // 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)); + + _setOwnerViaStorage(alice); + + // 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)); + } + + // ============ 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/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 new file mode 100644 index 0000000..b9ae946 --- /dev/null +++ b/docs/contracts/admin_proxy.md @@ -0,0 +1,380 @@ +# 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` set directly in storage slot 0. This eliminates any race condition and ensures the designated admin has control from block 0. + +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. + +## 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: Generate the Genesis Alloc Entry + +**Option A: Use the helper script (recommended)** + +Set the `OWNER` environment variable to your initial admin EOA address: + +```bash +OWNER=0xYourEOAAddress forge script script/GenerateAdminProxyAlloc.s.sol -vvv +``` + +This outputs the complete alloc entry with bytecode and storage, ready to 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...`. 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, 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": "0x", + "storage": { + "0x0": "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678" + } + } + } +} +``` + +**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 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. + +In this example, the owner EOA is `0xYourEOAAddressHere` (replace with your actual address): + +```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": { + "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`: + +```json +"storage": { + "0x0": "0x000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +} +``` + +### 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. **Verify owner is set correctly**: + + ```bash + cast call 0x000000000000000000000000000000000000Ad00 "owner()" --rpc-url + # Should return your EOA address (e.g., 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) + ``` + +### 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 + +Since the owner is set at genesis, no claiming is required. The designated EOA can immediately use the proxy. + +### 1. Verify Ownership + +Confirm the owner was set correctly: + +```bash +cast call 0x000000000000000000000000000000000000Ad00 "owner()" --rpc-url +# Should return your EOA address +``` + +### 2. Deploy Multisig + +Deploy your multisig (e.g., Safe) through normal transaction flow. + +### 3. Transfer Ownership + +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) +// 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 + +### 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(); +``` + +### Genesis Storage Initialization + +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 + +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 | +|----------|-------------|--------| +| `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 | +| `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". 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" + } + } }