Skip to content

Commit 04beb6b

Browse files
committed
refactor(contracts): remove Hyperlane dependency from FeeVault
Remove IHypNativeMinter interface and all Hyperlane-specific fields (destinationDomain, recipientAddress) from FeeVault. The contract now uses direct ETH transfers to a configurable bridgeRecipient instead of Hyperlane's transferRemote(). sendToCelestia() renamed to distribute(). Also removes the hyperlane-monorepo git submodule and updates all deployment scripts, tests, and documentation.
1 parent c32b633 commit 04beb6b

12 files changed

Lines changed: 132 additions & 284 deletions

File tree

.claude/skills/contracts.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: This skill should be used when the user asks about "ev-reth contracts", "FeeVault", "AdminProxy", "fee bridging to Celestia", "Hyperlane integration", "Foundry deployment scripts", "genesis allocations", or wants to understand how base fees are redirected and bridged.
2+
description: This skill should be used when the user asks about "ev-reth contracts", "FeeVault", "AdminProxy", "fee distribution", "Foundry deployment scripts", "genesis allocations", or wants to understand how base fees are redirected and distributed.
33
---
44

55
# Contracts Onboarding
@@ -9,13 +9,13 @@ description: This skill should be used when the user asks about "ev-reth contrac
99
The contracts live in `contracts/` and use Foundry for development. There are two main contracts:
1010

1111
1. **AdminProxy** (`src/AdminProxy.sol`) - Bootstrap contract for admin addresses at genesis
12-
2. **FeeVault** (`src/FeeVault.sol`) - Collects base fees, bridges to Celestia via Hyperlane (cross-chain messaging protocol)
12+
2. **FeeVault** (`src/FeeVault.sol`) - Collects base fees and distributes them between configured recipients
1313

1414
## Key Files
1515

1616
### Contract Sources
1717
- `contracts/src/AdminProxy.sol` - Transparent proxy pattern for admin control
18-
- `contracts/src/FeeVault.sol` - Fee collection and bridging logic
18+
- `contracts/src/FeeVault.sol` - Fee collection and distribution logic
1919

2020
### Deployment Scripts
2121
- `contracts/script/DeployFeeVault.s.sol` - FeeVault deployment with CREATE2
@@ -34,7 +34,7 @@ The AdminProxy contract provides a bootstrap mechanism for setting admin address
3434
### FeeVault
3535
The FeeVault serves as the destination for redirected base fees (instead of burning them). Key responsibilities:
3636
- Receive base fees from block production
37-
- Bridge accumulated fees to Celestia via Hyperlane
37+
- Distribute accumulated fees between configured recipients
3838
- Manage withdrawal permissions
3939

4040
## Connection to Rust Code

.gitmodules

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
[submodule "contracts/lib/forge-std"]
22
path = contracts/lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4-
[submodule "contracts/lib/hyperlane-monorepo"]
5-
path = contracts/lib/hyperlane-monorepo
6-
url = https://github.com/hyperlane-xyz/hyperlane-monorepo.git
74
[submodule "contracts/lib/permit2"]
85
path = contracts/lib/permit2
96
url = https://github.com/Uniswap/permit2

contracts/README.md

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# EV-Reth Contracts
22

3-
Smart contracts for EV-Reth, including the FeeVault for bridging collected fees to Celestia.
3+
Smart contracts for EV-Reth, including the FeeVault for collecting and distributing fees.
44

55
## AdminProxy
66

@@ -10,11 +10,11 @@ See [AdminProxy documentation](../docs/contracts/admin_proxy.md) for detailed se
1010

1111
## FeeVault
1212

13-
The FeeVault contract collects base fees and bridges them to Celestia via Hyperlane. It supports:
13+
The FeeVault contract collects base fees and distributes them between a bridge recipient and an optional secondary recipient. It supports:
1414

1515
- Configurable fee splitting between bridge and another recipient
16-
- Minimum amount thresholds before bridging
17-
- Call fee for incentivizing bridge calls
16+
- Minimum amount thresholds before distributing
17+
- Call fee for incentivizing distribution calls
1818
- Owner-controlled configuration
1919

2020
## Prerequisites
@@ -45,17 +45,14 @@ All configuration is set via constructor arguments at deploy time:
4545
|----------|----------|-------------|
4646
| `OWNER` | Yes | Owner address (can configure the vault post-deployment) |
4747
| `SALT` | No | CREATE2 salt (default: `0x0`). Use any bytes32 value |
48-
| `DESTINATION_DOMAIN` | Yes* | Hyperlane destination chain ID |
49-
| `RECIPIENT_ADDRESS` | Yes* | Recipient on destination chain (bytes32, left-padded) |
50-
| `MINIMUM_AMOUNT` | No | Minimum wei to bridge (default: 0) |
51-
| `CALL_FEE` | No | Fee in wei for calling `sendToCelestia()` (default: 0) |
48+
| `MINIMUM_AMOUNT` | No | Minimum wei to distribute (default: 0) |
49+
| `CALL_FEE` | No | Fee in wei for calling `distribute()` (default: 0) |
5250
| `BRIDGE_SHARE_BPS` | No | Basis points to bridge (default: 10000 = 100%) |
53-
| `OTHER_RECIPIENT` | No** | Address to receive non-bridged portion |
51+
| `OTHER_RECIPIENT` | No* | Address to receive non-bridged portion |
5452

55-
*Required for the vault to be operational (can be set to 0 at deploy and configured later via setters)
56-
**Required if `BRIDGE_SHARE_BPS` < 10000
53+
*Required if `BRIDGE_SHARE_BPS` < 10000
5754

58-
**Note:** `HYP_NATIVE_MINTER` must be set via `setHypNativeMinter()` after deployment for the vault to be operational.
55+
**Note:** `BRIDGE_RECIPIENT` must be set via `setBridgeRecipient()` after deployment for the vault to be operational.
5956

6057
### Choosing a Salt
6158

@@ -89,8 +86,6 @@ export OWNER=0xYourOwnerAddress
8986
export SALT=0x0000000000000000000000000000000000000000000000000000000000000001
9087

9188
# Optional - configure at deploy time (can also be set later)
92-
export DESTINATION_DOMAIN=1234
93-
export RECIPIENT_ADDRESS=0x000000000000000000000000... # bytes32, left-padded cosmos address
9489
export MINIMUM_AMOUNT=1000000000000000000 # 1 ETH in wei
9590
export CALL_FEE=100000000000000 # 0.0001 ETH
9691
export BRIDGE_SHARE_BPS=8000 # 80% to bridge
@@ -107,43 +102,25 @@ forge script script/DeployFeeVault.s.sol:DeployFeeVault \
107102
--broadcast
108103
```
109104

110-
### Post-Deployment: Set HypNativeMinter
105+
### Post-Deployment: Set Bridge Recipient
111106

112-
After deploying the HypNativeMinter contract, link it to the FeeVault:
107+
After deployment, set the bridge recipient address:
113108

114109
```shell
115-
cast send <FEEVAULT_ADDRESS> "setHypNativeMinter(address)" <HYP_NATIVE_MINTER_ADDRESS> \
110+
cast send <FEEVAULT_ADDRESS> "setBridgeRecipient(address)" <BRIDGE_RECIPIENT_ADDRESS> \
116111
--rpc-url <RPC_URL> \
117112
--private-key <PRIVATE_KEY>
118113
```
119114

120-
### Converting Cosmos Addresses to bytes32
121-
122-
The `recipientAddress` must be a bytes32. To convert a bech32 Cosmos address:
123-
124-
1. Decode the bech32 to get the 20-byte address
125-
2. Left-pad with zeros to 32 bytes
126-
127-
Example using cast:
128-
129-
```shell
130-
# Left-pad a 20-byte address to 32 bytes
131-
cast pad --left --len 32 1234567890abcdef1234567890abcdef12345678
132-
# Output: 0x0000000000000000000000001234567890abcdef1234567890abcdef12345678
133-
```
134-
135-
Note: When calling `transferRemote()` via cast, you may need to omit the `0x` prefix depending on your invocation method.
136-
137115
## Admin Functions
138116

139117
All functions are owner-only:
140118

141119
| Function | Description |
142120
|----------|-------------|
143-
| `setHypNativeMinter(address)` | Set the Hyperlane minter contract |
144-
| `setRecipient(uint32, bytes32)` | Set destination domain and recipient |
145-
| `setMinimumAmount(uint256)` | Set minimum amount to bridge |
146-
| `setCallFee(uint256)` | Set fee for calling sendToCelestia |
121+
| `setBridgeRecipient(address)` | Set the bridge recipient address |
122+
| `setMinimumAmount(uint256)` | Set minimum amount to distribute |
123+
| `setCallFee(uint256)` | Set fee for calling distribute |
147124
| `setBridgeShare(uint256)` | Set bridge percentage (basis points) |
148125
| `setOtherRecipient(address)` | Set recipient for non-bridged funds |
149126
| `transferOwnership(address)` | Transfer contract ownership |

contracts/foundry.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"lib/forge-std": {
33
"rev": "887e87251562513a7b5ab1ea517c039fe6ee0984"
4+
},
5+
"lib/permit2": {
6+
"rev": "cc56ad0f3439c502c246fc5cfcc3db92bb8b7219"
47
}
58
}

contracts/lib/hyperlane-monorepo

Lines changed: 0 additions & 1 deletion
This file was deleted.

contracts/script/DeployFeeVault.s.sol

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ contract DeployFeeVault is Script {
1010
address owner = vm.envAddress("OWNER");
1111
bytes32 salt = vm.envOr("SALT", bytes32(0));
1212

13-
uint32 destinationDomain = uint32(vm.envOr("DESTINATION_DOMAIN", uint256(0)));
14-
bytes32 recipientAddress = vm.envOr("RECIPIENT_ADDRESS", bytes32(0));
1513
uint256 minimumAmount = vm.envOr("MINIMUM_AMOUNT", uint256(0));
1614
uint256 callFee = vm.envOr("CALL_FEE", uint256(0));
1715
uint256 bridgeShareBps = vm.envOr("BRIDGE_SHARE_BPS", uint256(0)); // 0 defaults to 10000 in constructor
@@ -21,34 +19,28 @@ contract DeployFeeVault is Script {
2119
vm.startBroadcast();
2220

2321
// Deploy FeeVault with CREATE2
24-
FeeVault feeVault = new FeeVault{salt: salt}(
25-
owner, destinationDomain, recipientAddress, minimumAmount, callFee, bridgeShareBps, otherRecipient
26-
);
22+
FeeVault feeVault =
23+
new FeeVault{salt: salt}(owner, minimumAmount, callFee, bridgeShareBps, otherRecipient);
2724

2825
vm.stopBroadcast();
2926

3027
console.log("FeeVault deployed at:", address(feeVault));
3128
console.log("Owner:", owner);
32-
console.log("Destination domain:", destinationDomain);
3329
console.log("Minimum amount:", minimumAmount);
3430
console.log("Call fee:", callFee);
3531
console.log("Bridge share bps:", feeVault.bridgeShareBps());
3632
console.log("");
37-
console.log("NOTE: Call setHypNativeMinter() after deploying HypNativeMinter");
33+
console.log("NOTE: Call setBridgeRecipient() to set the bridge destination");
3834
}
3935
}
4036

4137
/// @notice Compute FeeVault CREATE2 address off-chain
42-
/// @dev Use this to predict the address before deploying
43-
/// Requires env vars: DEPLOYER (EOA), OWNER, SALT (optional), and all constructor args
4438
contract ComputeFeeVaultAddress is Script {
4539
function run() external view {
4640
address deployer = vm.envAddress("DEPLOYER");
4741
bytes32 salt = vm.envOr("SALT", bytes32(0));
4842

4943
address owner = vm.envAddress("OWNER");
50-
uint32 destinationDomain = uint32(vm.envOr("DESTINATION_DOMAIN", uint256(0)));
51-
bytes32 recipientAddress = vm.envOr("RECIPIENT_ADDRESS", bytes32(0));
5244
uint256 minimumAmount = vm.envOr("MINIMUM_AMOUNT", uint256(0));
5345
uint256 callFee = vm.envOr("CALL_FEE", uint256(0));
5446
uint256 bridgeShareBps = vm.envOr("BRIDGE_SHARE_BPS", uint256(0));
@@ -57,9 +49,7 @@ contract ComputeFeeVaultAddress is Script {
5749
bytes32 initCodeHash = keccak256(
5850
abi.encodePacked(
5951
type(FeeVault).creationCode,
60-
abi.encode(
61-
owner, destinationDomain, recipientAddress, minimumAmount, callFee, bridgeShareBps, otherRecipient
62-
)
52+
abi.encode(owner, minimumAmount, callFee, bridgeShareBps, otherRecipient)
6353
)
6454
);
6555

contracts/script/GenerateFeeVaultAlloc.s.sol

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,23 @@ abstract contract FeeVaultAllocBase is Script {
88
struct Config {
99
address feeVaultAddress;
1010
address owner;
11-
uint32 destinationDomain;
12-
bytes32 recipientAddress;
11+
address bridgeRecipient;
12+
address otherRecipient;
1313
uint256 minimumAmount;
1414
uint256 callFee;
1515
uint256 bridgeShareBpsRaw;
1616
uint256 bridgeShareBps;
17-
address otherRecipient;
18-
address hypNativeMinter;
1917
bytes32 salt;
2018
address deployer;
2119
}
2220

2321
function loadConfig() internal view returns (Config memory cfg) {
2422
cfg.owner = vm.envAddress("OWNER");
25-
cfg.destinationDomain = uint32(vm.envOr("DESTINATION_DOMAIN", uint256(0)));
26-
cfg.recipientAddress = vm.envOr("RECIPIENT_ADDRESS", bytes32(0));
23+
cfg.bridgeRecipient = vm.envOr("BRIDGE_RECIPIENT", address(0));
24+
cfg.otherRecipient = vm.envOr("OTHER_RECIPIENT", address(0));
2725
cfg.minimumAmount = vm.envOr("MINIMUM_AMOUNT", uint256(0));
2826
cfg.callFee = vm.envOr("CALL_FEE", uint256(0));
2927
cfg.bridgeShareBpsRaw = vm.envOr("BRIDGE_SHARE_BPS", uint256(0));
30-
cfg.otherRecipient = vm.envOr("OTHER_RECIPIENT", address(0));
31-
cfg.hypNativeMinter = vm.envOr("HYP_NATIVE_MINTER", address(0));
3228
cfg.feeVaultAddress = vm.envOr("FEE_VAULT_ADDRESS", address(0));
3329
cfg.deployer = vm.envOr("DEPLOYER", address(0));
3430
cfg.salt = vm.envOr("SALT", bytes32(0));
@@ -43,13 +39,7 @@ abstract contract FeeVaultAllocBase is Script {
4339
abi.encodePacked(
4440
type(FeeVault).creationCode,
4541
abi.encode(
46-
cfg.owner,
47-
cfg.destinationDomain,
48-
cfg.recipientAddress,
49-
cfg.minimumAmount,
50-
cfg.callFee,
51-
cfg.bridgeShareBpsRaw,
52-
cfg.otherRecipient
42+
cfg.owner, cfg.minimumAmount, cfg.callFee, cfg.bridgeShareBpsRaw, cfg.otherRecipient
5343
)
5444
)
5545
);
@@ -64,29 +54,19 @@ abstract contract FeeVaultAllocBase is Script {
6454
function computeSlots(Config memory cfg)
6555
internal
6656
pure
67-
returns (
68-
bytes32 slot0,
69-
bytes32 slot1,
70-
bytes32 slot2,
71-
bytes32 slot3,
72-
bytes32 slot4,
73-
bytes32 slot5,
74-
bytes32 slot6
75-
)
57+
returns (bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5)
7658
{
77-
slot0 = bytes32(uint256(uint160(cfg.hypNativeMinter)));
78-
slot1 = bytes32((uint256(cfg.destinationDomain) << 160) | uint256(uint160(cfg.owner)));
79-
slot2 = cfg.recipientAddress;
59+
slot0 = bytes32(uint256(uint160(cfg.owner)));
60+
slot1 = bytes32(uint256(uint160(cfg.bridgeRecipient)));
61+
slot2 = bytes32(uint256(uint160(cfg.otherRecipient)));
8062
slot3 = bytes32(cfg.minimumAmount);
8163
slot4 = bytes32(cfg.callFee);
82-
slot5 = bytes32(uint256(uint160(cfg.otherRecipient)));
83-
slot6 = bytes32(cfg.bridgeShareBps);
64+
slot5 = bytes32(cfg.bridgeShareBps);
8465
}
8566

8667
function addressKey(address addr) internal pure returns (string memory) {
8768
bytes memory full = bytes(vm.toString(addr));
8869
bytes memory key = new bytes(40);
89-
// Fixed-length copy for address key without 0x prefix.
9070
for (uint256 i = 0; i < 40; i++) {
9171
key[i] = full[i + 2];
9272
}
@@ -102,13 +82,12 @@ contract GenerateFeeVaultAlloc is FeeVaultAllocBase {
10282
Config memory cfg = loadConfig();
10383
bytes memory runtimeCode = type(FeeVault).runtimeCode;
10484

105-
(bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5, bytes32 slot6) =
85+
(bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5) =
10686
computeSlots(cfg);
10787

10888
console.log("========== FeeVault Genesis Alloc ==========");
10989
console.log("FeeVault address:", cfg.feeVaultAddress);
11090
console.log("Owner:", cfg.owner);
111-
console.log("Destination domain:", cfg.destinationDomain);
11291
console.log("Bridge share bps (raw):", cfg.bridgeShareBpsRaw);
11392
console.log("Bridge share bps (effective):", cfg.bridgeShareBps);
11493
console.log("");
@@ -119,8 +98,8 @@ contract GenerateFeeVaultAlloc is FeeVaultAllocBase {
11998
if (cfg.bridgeShareBps < 10000 && cfg.otherRecipient == address(0)) {
12099
console.log("WARNING: OTHER_RECIPIENT is zero but bridge share < 10000.");
121100
}
122-
if (cfg.hypNativeMinter == address(0)) {
123-
console.log("NOTE: HYP_NATIVE_MINTER is zero; set it before calling sendToCelestia().");
101+
if (cfg.bridgeRecipient == address(0)) {
102+
console.log("NOTE: BRIDGE_RECIPIENT is zero; set it before calling distribute().");
124103
}
125104
console.log("");
126105

@@ -137,8 +116,7 @@ contract GenerateFeeVaultAlloc is FeeVaultAllocBase {
137116
console.log(' "0x2": "%s",', vm.toString(slot2));
138117
console.log(' "0x3": "%s",', vm.toString(slot3));
139118
console.log(' "0x4": "%s",', vm.toString(slot4));
140-
console.log(' "0x5": "%s",', vm.toString(slot5));
141-
console.log(' "0x6": "%s"', vm.toString(slot6));
119+
console.log(' "0x5": "%s"', vm.toString(slot5));
142120
console.log(" }");
143121
console.log(" }");
144122
console.log(" }");
@@ -157,7 +135,7 @@ contract GenerateFeeVaultAllocJSON is FeeVaultAllocBase {
157135
Config memory cfg = loadConfig();
158136
bytes memory runtimeCode = type(FeeVault).runtimeCode;
159137

160-
(bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5, bytes32 slot6) =
138+
(bytes32 slot0, bytes32 slot1, bytes32 slot2, bytes32 slot3, bytes32 slot4, bytes32 slot5) =
161139
computeSlots(cfg);
162140

163141
string memory json = string(
@@ -178,8 +156,6 @@ contract GenerateFeeVaultAllocJSON is FeeVaultAllocBase {
178156
vm.toString(slot4),
179157
'","0x5":"',
180158
vm.toString(slot5),
181-
'","0x6":"',
182-
vm.toString(slot6),
183159
'"}}}'
184160
)
185161
);

0 commit comments

Comments
 (0)