-
Notifications
You must be signed in to change notification settings - Fork 8
feat: add proxy contract #97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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",'); | ||
tac0turtle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Comment on lines
+74
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To adhere to the Checks-Effects-Interactions (CEI) pattern, state modifications should occur before external interactions like emitting events. This is a security best practice that helps prevent reentrancy vulnerabilities, even if no direct exploit is apparent here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would generally agree |
||
| } | ||
|
|
||
| /// @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 {} | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.