Skip to content

Commit 8962307

Browse files
authored
feat: add proxy contract (#97)
* add proxy contract * doc * remove claim ownership * lint * fixes
1 parent 2aad2c8 commit 8962307

File tree

7 files changed

+1122
-2
lines changed

7 files changed

+1122
-2
lines changed

contracts/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
Smart contracts for EV-Reth, including the FeeVault for bridging collected fees to Celestia.
44

5+
## AdminProxy
6+
7+
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.
8+
9+
See [AdminProxy documentation](../docs/contracts/admin_proxy.md) for detailed setup and usage instructions.
10+
511
## FeeVault
612

713
The FeeVault contract collects base fees and bridges them to Celestia via Hyperlane. It supports:
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {Script, console} from "forge-std/Script.sol";
5+
import {AdminProxy} from "../src/AdminProxy.sol";
6+
7+
/// @title GenerateAdminProxyAlloc
8+
/// @notice Generates genesis alloc JSON for deploying AdminProxy at a deterministic address
9+
/// @dev Run with: OWNER=0xYourAddress forge script script/GenerateAdminProxyAlloc.s.sol -vvv
10+
///
11+
/// This script outputs the bytecode and storage layout needed to deploy AdminProxy
12+
/// in the genesis block. The owner is set directly in storage slot 0.
13+
///
14+
/// Usage:
15+
/// 1. Set OWNER env var to your initial admin EOA address
16+
/// 2. Run this script to get the bytecode and storage
17+
/// 3. Add to genesis.json alloc section at desired address (e.g., 0x...Ad00)
18+
/// 4. Set that address as mintAdmin in chainspec config
19+
contract GenerateAdminProxyAlloc is Script {
20+
// Suggested deterministic address for AdminProxy
21+
// Using a memorable address in the precompile-adjacent range
22+
address constant SUGGESTED_ADDRESS = 0x000000000000000000000000000000000000Ad00;
23+
24+
function run() external {
25+
// Get owner from environment, default to zero if not set
26+
address owner = vm.envOr("OWNER", address(0));
27+
28+
// Deploy to get runtime bytecode
29+
AdminProxy proxy = new AdminProxy();
30+
31+
// Get runtime bytecode (not creation code)
32+
bytes memory runtimeCode = address(proxy).code;
33+
34+
// Convert owner to storage slot value (left-padded to 32 bytes)
35+
bytes32 ownerSlotValue = bytes32(uint256(uint160(owner)));
36+
37+
console.log("========== AdminProxy Genesis Alloc ==========");
38+
console.log("");
39+
console.log("Suggested address:", SUGGESTED_ADDRESS);
40+
console.log("Owner (from OWNER env):", owner);
41+
console.log("");
42+
43+
if (owner == address(0)) {
44+
console.log("WARNING: OWNER not set! Set OWNER env var to your admin EOA.");
45+
console.log("Example: OWNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 forge script ...");
46+
console.log("");
47+
}
48+
49+
console.log("Add this to your genesis.json 'alloc' section:");
50+
console.log("");
51+
console.log("{");
52+
console.log(' "alloc": {');
53+
console.log(' "000000000000000000000000000000000000Ad00": {');
54+
console.log(' "balance": "0x0",');
55+
console.log(' "code": "0x%s",', vm.toString(runtimeCode));
56+
console.log(' "storage": {');
57+
console.log(' "0x0": "0x%s"', vm.toString(ownerSlotValue));
58+
console.log(" }");
59+
console.log(" }");
60+
console.log(" }");
61+
console.log("}");
62+
console.log("");
63+
console.log("Then update chainspec config:");
64+
console.log("");
65+
console.log("{");
66+
console.log(' "config": {');
67+
console.log(' "evolve": {');
68+
console.log(' "mintAdmin": "0x000000000000000000000000000000000000Ad00",');
69+
console.log(' "mintPrecompileActivationHeight": 0');
70+
console.log(" }");
71+
console.log(" }");
72+
console.log("}");
73+
console.log("");
74+
console.log("==============================================");
75+
console.log("");
76+
console.log("Post-genesis steps:");
77+
console.log("1. Owner can immediately use the proxy (no claiming needed)");
78+
console.log("2. Deploy multisig (e.g., Safe)");
79+
console.log("3. Call transferOwnership(multisigAddress)");
80+
console.log("4. From multisig, call acceptOwnership()");
81+
console.log("");
82+
83+
// Also output raw values for programmatic use
84+
console.log("Raw bytecode length:", runtimeCode.length);
85+
console.log("Owner storage slot (0x0):", vm.toString(ownerSlotValue));
86+
}
87+
}
88+
89+
/// @title GenerateAdminProxyAllocJSON
90+
/// @notice Outputs just the JSON snippet for easy copy-paste
91+
/// @dev Run with: OWNER=0xYourAddress forge script script/GenerateAdminProxyAlloc.s.sol:GenerateAdminProxyAllocJSON -vvv
92+
contract GenerateAdminProxyAllocJSON is Script {
93+
function run() external {
94+
address owner = vm.envOr("OWNER", address(0));
95+
96+
AdminProxy proxy = new AdminProxy();
97+
bytes memory runtimeCode = address(proxy).code;
98+
bytes32 ownerSlotValue = bytes32(uint256(uint160(owner)));
99+
100+
// Output minimal JSON that can be merged into genesis
101+
string memory json = string(
102+
abi.encodePacked(
103+
'{"000000000000000000000000000000000000Ad00":{"balance":"0x0","code":"0x',
104+
vm.toString(runtimeCode),
105+
'","storage":{"0x0":"0x',
106+
vm.toString(ownerSlotValue),
107+
'"}}}'
108+
)
109+
);
110+
111+
console.log(json);
112+
}
113+
}

contracts/src/AdminProxy.sol

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
/// @title AdminProxy
5+
/// @notice A proxy contract for managing admin rights to precompiles and other contracts.
6+
/// @dev Deployed at genesis with owner set via storage slot. Supports two-step
7+
/// ownership transfer for safe handoff to multisigs or other governance contracts.
8+
///
9+
/// This contract solves the bootstrap problem where admin addresses (e.g., multisigs)
10+
/// are not known at genesis time. The proxy is set as admin in the chainspec, and
11+
/// an initial EOA owner is set in genesis storage. Post-genesis, ownership can be
12+
/// transferred to a multisig.
13+
///
14+
/// Storage Layout:
15+
/// - Slot 0: owner (address)
16+
/// - Slot 1: pendingOwner (address)
17+
///
18+
/// Usage:
19+
/// 1. Deploy at genesis via alloc with owner set in storage slot 0
20+
/// 2. Set proxy address as `mintAdmin` in chainspec and as FeeVault owner
21+
/// 3. Post-genesis: deploy multisig, then transferOwnership() -> acceptOwnership()
22+
contract AdminProxy {
23+
/// @notice Current owner of the proxy
24+
address public owner;
25+
26+
/// @notice Pending owner for two-step transfer
27+
address public pendingOwner;
28+
29+
/// @notice Emitted when ownership transfer is initiated
30+
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
31+
32+
/// @notice Emitted when ownership transfer is completed
33+
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
34+
35+
/// @notice Emitted when a call is executed through the proxy
36+
event Executed(address indexed target, bytes data, bytes result);
37+
38+
/// @notice Thrown when caller is not the owner
39+
error NotOwner();
40+
41+
/// @notice Thrown when caller is not the pending owner
42+
error NotPendingOwner();
43+
44+
/// @notice Thrown when a call to target contract fails
45+
error CallFailed(bytes reason);
46+
47+
/// @notice Thrown when array lengths don't match in batch operations
48+
error LengthMismatch();
49+
50+
/// @notice Thrown when trying to set zero address as pending owner
51+
error ZeroAddress();
52+
53+
modifier onlyOwner() {
54+
if (msg.sender != owner) revert NotOwner();
55+
_;
56+
}
57+
58+
/// @notice Constructor is empty - owner is set via genesis storage slot 0
59+
/// @dev When deploying at genesis, set storage["0x0"] to the owner address
60+
constructor() {}
61+
62+
/// @notice Start two-step ownership transfer
63+
/// @param newOwner Address of the new owner (e.g., multisig)
64+
function transferOwnership(address newOwner) external onlyOwner {
65+
if (newOwner == address(0)) revert ZeroAddress();
66+
pendingOwner = newOwner;
67+
emit OwnershipTransferStarted(owner, newOwner);
68+
}
69+
70+
/// @notice Complete two-step ownership transfer
71+
/// @dev Must be called by the pending owner
72+
function acceptOwnership() external {
73+
if (msg.sender != pendingOwner) revert NotPendingOwner();
74+
emit OwnershipTransferred(owner, msg.sender);
75+
owner = msg.sender;
76+
pendingOwner = address(0);
77+
}
78+
79+
/// @notice Cancel pending ownership transfer
80+
function cancelTransfer() external onlyOwner {
81+
pendingOwner = address(0);
82+
}
83+
84+
/// @notice Execute a call to any target contract
85+
/// @param target Address of the contract to call
86+
/// @param data Calldata to send
87+
/// @return result The return data from the call
88+
/// @dev Use this to call admin functions on FeeVault, precompiles, etc.
89+
///
90+
/// Example - Add address to mint precompile allowlist:
91+
/// execute(MINT_PRECOMPILE, abi.encodeCall(IMintPrecompile.addToAllowList, (account)))
92+
///
93+
/// Example - Transfer FeeVault ownership:
94+
/// execute(feeVault, abi.encodeCall(FeeVault.transferOwnership, (newOwner)))
95+
function execute(address target, bytes calldata data) external onlyOwner returns (bytes memory result) {
96+
(bool success, bytes memory returnData) = target.call(data);
97+
if (!success) {
98+
revert CallFailed(returnData);
99+
}
100+
emit Executed(target, data, returnData);
101+
return returnData;
102+
}
103+
104+
/// @notice Execute multiple calls in a single transaction
105+
/// @param targets Array of contract addresses to call
106+
/// @param datas Array of calldata for each call
107+
/// @return results Array of return data from each call
108+
/// @dev Useful for batch operations like adding multiple addresses to allowlist
109+
function executeBatch(address[] calldata targets, bytes[] calldata datas)
110+
external
111+
onlyOwner
112+
returns (bytes[] memory results)
113+
{
114+
if (targets.length != datas.length) revert LengthMismatch();
115+
116+
results = new bytes[](targets.length);
117+
for (uint256 i = 0; i < targets.length; i++) {
118+
(bool success, bytes memory returnData) = targets[i].call(datas[i]);
119+
if (!success) {
120+
revert CallFailed(returnData);
121+
}
122+
emit Executed(targets[i], datas[i], returnData);
123+
results[i] = returnData;
124+
}
125+
}
126+
127+
/// @notice Execute a call with ETH value
128+
/// @param target Address of the contract to call
129+
/// @param data Calldata to send
130+
/// @param value Amount of ETH to send
131+
/// @return result The return data from the call
132+
function executeWithValue(address target, bytes calldata data, uint256 value)
133+
external
134+
onlyOwner
135+
returns (bytes memory result)
136+
{
137+
(bool success, bytes memory returnData) = target.call{value: value}(data);
138+
if (!success) {
139+
revert CallFailed(returnData);
140+
}
141+
emit Executed(target, data, returnData);
142+
return returnData;
143+
}
144+
145+
/// @notice Receive ETH (needed for executeWithValue)
146+
receive() external payable {}
147+
}

0 commit comments

Comments
 (0)