Skip to content

Commit b9c1f6c

Browse files
authored
Merge branch 'main' into marko/reth2.2
2 parents 10ae4cd + 5ea1341 commit b9c1f6c

23 files changed

Lines changed: 644 additions & 315 deletions

.claude/skills/contracts.md

Lines changed: 9 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", "Permit2", "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,15 @@ 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
13+
3. **Permit2** (`lib/permit2`) - Uniswap's canonical token approval manager, deployed at genesis via `ev-deployer` (no Foundry deploy script — bytecode is embedded in Rust)
1314

1415
## Key Files
1516

1617
### Contract Sources
1718
- `contracts/src/AdminProxy.sol` - Transparent proxy pattern for admin control
18-
- `contracts/src/FeeVault.sol` - Fee collection and bridging logic
19+
- `contracts/src/FeeVault.sol` - Fee collection and distribution logic
20+
- `contracts/lib/permit2` - Uniswap Permit2 submodule (bytecode used by ev-deployer)
1921

2022
### Deployment Scripts
2123
- `contracts/script/DeployFeeVault.s.sol` - FeeVault deployment with CREATE2
@@ -34,9 +36,12 @@ The AdminProxy contract provides a bootstrap mechanism for setting admin address
3436
### FeeVault
3537
The FeeVault serves as the destination for redirected base fees (instead of burning them). Key responsibilities:
3638
- Receive base fees from block production
37-
- Bridge accumulated fees to Celestia via Hyperlane
39+
- Distribute accumulated fees between configured recipients
3840
- Manage withdrawal permissions
3941

42+
### Permit2
43+
Uniswap's canonical token approval manager deployed at genesis. Unlike AdminProxy and FeeVault, Permit2 has no Foundry deploy script — its bytecode is embedded directly in the Rust `ev-deployer` (`bin/ev-deployer/src/contracts/permit2.rs`), which patches EIP-712 immutables (chain ID, domain separator) at genesis time.
44+
4045
## Connection to Rust Code
4146

4247
The contracts integrate with ev-reth through:

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +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/permit2"]
5+
path = contracts/lib/permit2
6+
url = https://github.com/Uniswap/permit2

bin/ev-deployer/README.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@ The binary is output to `target/release/ev-deployer`.
1414

1515
EV Deployer uses a TOML config file to define what contracts to include and how to configure them. See [`examples/devnet.toml`](examples/devnet.toml) for a complete example.
1616

17-
```toml
18-
[chain]
19-
chain_id = 1234
20-
21-
[contracts.admin_proxy]
22-
address = "0x000000000000000000000000000000000000Ad00"
23-
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
24-
```
25-
2617
### Config reference
2718

2819
#### `[chain]`
@@ -38,6 +29,12 @@ owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
3829
| `address` | address | Address to deploy at |
3930
| `owner` | address | Owner (must not be zero) |
4031

32+
#### `[contracts.permit2]`
33+
34+
| Field | Type | Description |
35+
|-----------|---------|----------------------------------------------------------|
36+
| `address` | address | Address to deploy at (canonical: `0x000000000022D473030F116dDEE9F6B43aC78BA3`) |
37+
4138
## Usage
4239

4340
### Generate a starter config
@@ -88,7 +85,8 @@ Output:
8885

8986
```json
9087
{
91-
"admin_proxy": "0x000000000000000000000000000000000000Ad00"
88+
"admin_proxy": "0x000000000000000000000000000000000000Ad00",
89+
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
9290
}
9391
```
9492

@@ -100,9 +98,10 @@ ev-deployer compute-address --config deploy.toml --contract admin_proxy
10098

10199
## Contracts
102100

103-
| Contract | Description |
104-
|----------------|-----------------------------------------------------|
105-
| `admin_proxy` | Proxy contract with owner-based access control |
101+
| Contract | Description |
102+
|---------------|----------------------------------------------------|
103+
| `admin_proxy` | Proxy contract with owner-based access control |
104+
| `permit2` | Uniswap canonical token approval manager |
106105

107106
Runtime bytecodes are embedded in the binary — no external toolchain is needed at deploy time.
108107

bin/ev-deployer/examples/devnet.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ chain_id = 1234
44
[contracts.admin_proxy]
55
address = "0x000000000000000000000000000000000000Ad00"
66
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
7+
8+
[contracts.permit2]
9+
# Canonical Uniswap Permit2 address (same on all chains via CREATE2).
10+
# Using it here so frontends, SDKs and routers work out of the box.
11+
address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"

bin/ev-deployer/src/config.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
33
use alloy_primitives::Address;
44
use serde::Deserialize;
5-
use std::path::Path;
5+
use std::{collections::HashSet, path::Path};
66

77
/// Top-level deploy configuration.
88
#[derive(Debug, Deserialize)]
9-
#[allow(dead_code)]
109
pub(crate) struct DeployConfig {
1110
/// Chain configuration.
1211
pub chain: ChainConfig,
@@ -17,7 +16,6 @@ pub(crate) struct DeployConfig {
1716

1817
/// Chain-level settings.
1918
#[derive(Debug, Deserialize)]
20-
#[allow(dead_code)]
2119
pub(crate) struct ChainConfig {
2220
/// The chain ID.
2321
pub chain_id: u64,
@@ -28,6 +26,22 @@ pub(crate) struct ChainConfig {
2826
pub(crate) struct ContractsConfig {
2927
/// `AdminProxy` contract config (optional).
3028
pub admin_proxy: Option<AdminProxyConfig>,
29+
/// `Permit2` contract config (optional).
30+
pub permit2: Option<Permit2Config>,
31+
}
32+
33+
impl ContractsConfig {
34+
/// Collect all configured deploy addresses.
35+
fn all_addresses(&self) -> Vec<Address> {
36+
let mut addrs = Vec::new();
37+
if let Some(ref ap) = self.admin_proxy {
38+
addrs.push(ap.address);
39+
}
40+
if let Some(ref p2) = self.permit2 {
41+
addrs.push(p2.address);
42+
}
43+
addrs
44+
}
3145
}
3246

3347
/// `AdminProxy` configuration.
@@ -39,6 +53,13 @@ pub(crate) struct AdminProxyConfig {
3953
pub owner: Address,
4054
}
4155

56+
/// `Permit2` configuration (Uniswap token approval manager).
57+
#[derive(Debug, Deserialize)]
58+
pub(crate) struct Permit2Config {
59+
/// Address to deploy at.
60+
pub address: Address,
61+
}
62+
4263
impl DeployConfig {
4364
/// Load and validate config from a TOML file.
4465
pub(crate) fn load(path: &Path) -> eyre::Result<Self> {
@@ -57,6 +78,19 @@ impl DeployConfig {
5778
);
5879
}
5980

81+
if let Some(ref p2) = self.contracts.permit2 {
82+
eyre::ensure!(
83+
!p2.address.is_zero(),
84+
"permit2.address must not be the zero address"
85+
);
86+
}
87+
88+
// Detect duplicate deploy addresses across all contracts.
89+
let mut seen = HashSet::new();
90+
for addr in self.contracts.all_addresses() {
91+
eyre::ensure!(seen.insert(addr), "duplicate deploy address: {addr}");
92+
}
93+
6094
Ok(())
6195
}
6296
}
@@ -106,6 +140,71 @@ chain_id = 1
106140
assert!(config.contracts.admin_proxy.is_none());
107141
}
108142

143+
#[test]
144+
fn reject_zero_permit2_address() {
145+
let toml = r#"
146+
[chain]
147+
chain_id = 1
148+
149+
[contracts.permit2]
150+
address = "0x0000000000000000000000000000000000000000"
151+
"#;
152+
let config: DeployConfig = toml::from_str(toml).unwrap();
153+
assert!(config.validate().is_err());
154+
}
155+
156+
#[test]
157+
fn reject_duplicate_deploy_address() {
158+
let toml = r#"
159+
[chain]
160+
chain_id = 1
161+
162+
[contracts.admin_proxy]
163+
address = "0x000000000000000000000000000000000000Ad00"
164+
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
165+
166+
[contracts.permit2]
167+
address = "0x000000000000000000000000000000000000Ad00"
168+
"#;
169+
let config: DeployConfig = toml::from_str(toml).unwrap();
170+
let err = config.validate().unwrap_err().to_string();
171+
assert!(err.contains("duplicate deploy address"), "{err}");
172+
}
173+
174+
#[test]
175+
fn permit2_only() {
176+
let toml = r#"
177+
[chain]
178+
chain_id = 1
179+
180+
[contracts.permit2]
181+
address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
182+
"#;
183+
let config: DeployConfig = toml::from_str(toml).unwrap();
184+
config.validate().unwrap();
185+
assert!(config.contracts.permit2.is_some());
186+
assert!(config.contracts.admin_proxy.is_none());
187+
}
188+
189+
#[test]
190+
fn both_contracts() {
191+
let toml = r#"
192+
[chain]
193+
chain_id = 1
194+
195+
[contracts.admin_proxy]
196+
address = "0x000000000000000000000000000000000000Ad00"
197+
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
198+
199+
[contracts.permit2]
200+
address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
201+
"#;
202+
let config: DeployConfig = toml::from_str(toml).unwrap();
203+
config.validate().unwrap();
204+
assert!(config.contracts.admin_proxy.is_some());
205+
assert!(config.contracts.permit2.is_some());
206+
}
207+
109208
#[test]
110209
fn admin_proxy_only() {
111210
let toml = r#"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Bytecode patching for Solidity immutable variables.
2+
//!
3+
//! Solidity `immutable` values are embedded in the **runtime bytecode** by the
4+
//! compiler, not in storage. When compiling with placeholder values (e.g.
5+
//! `address(0)`, `uint32(0)`), the compiler leaves zero-filled regions at known
6+
//! byte offsets. This module replaces those regions with the actual values from
7+
//! the deploy config at genesis-generation time.
8+
9+
use alloy_primitives::{B256, U256};
10+
11+
/// A single immutable reference inside a bytecode blob.
12+
#[derive(Debug, Clone, Copy)]
13+
pub(crate) struct ImmutableRef {
14+
/// Byte offset into the **runtime** bytecode.
15+
pub start: usize,
16+
/// Number of bytes (always 32 for EVM words).
17+
pub length: usize,
18+
}
19+
20+
/// Patch a mutable bytecode slice, writing `value` at every listed offset.
21+
///
22+
/// # Panics
23+
///
24+
/// Panics if any reference extends past the end of `bytecode`.
25+
pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) {
26+
for r in refs {
27+
assert!(
28+
r.start + r.length <= bytecode.len(),
29+
"immutable ref out of bounds: start={} length={} bytecode_len={}",
30+
r.start,
31+
r.length,
32+
bytecode.len()
33+
);
34+
bytecode[r.start..r.start + r.length].copy_from_slice(value);
35+
}
36+
}
37+
38+
/// Convenience: patch with an ABI-encoded `uint256`.
39+
pub(crate) fn patch_u256(bytecode: &mut [u8], refs: &[ImmutableRef], val: U256) {
40+
let word = B256::from(val);
41+
patch_bytes(bytecode, refs, &word.0);
42+
}
43+
44+
#[cfg(test)]
45+
mod tests {
46+
use super::*;
47+
48+
#[test]
49+
fn patch_single_ref() {
50+
let mut bytecode = vec![0u8; 64];
51+
let refs = [ImmutableRef {
52+
start: 10,
53+
length: 32,
54+
}];
55+
let value = B256::from(U256::from(42u64));
56+
patch_bytes(&mut bytecode, &refs, &value.0);
57+
58+
assert_eq!(bytecode[41], 42);
59+
// bytes before are untouched
60+
assert_eq!(bytecode[9], 0);
61+
// bytes after are untouched
62+
assert_eq!(bytecode[42], 0);
63+
}
64+
65+
#[test]
66+
#[should_panic(expected = "immutable ref out of bounds")]
67+
fn patch_out_of_bounds_panics() {
68+
let mut bytecode = vec![0u8; 16];
69+
let refs = [ImmutableRef {
70+
start: 0,
71+
length: 32,
72+
}];
73+
let value = [0u8; 32];
74+
patch_bytes(&mut bytecode, &refs, &value);
75+
}
76+
}

bin/ev-deployer/src/contracts/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Contract bytecode and storage encoding.
22
33
pub(crate) mod admin_proxy;
4+
pub(crate) mod immutables;
5+
pub(crate) mod permit2;
46

57
use alloy_primitives::{Address, Bytes, B256};
68
use std::collections::BTreeMap;

bin/ev-deployer/src/contracts/permit2.rs

Lines changed: 233 additions & 0 deletions
Large diffs are not rendered by default.

bin/ev-deployer/src/genesis.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ pub(crate) fn build_alloc(config: &DeployConfig) -> Value {
1717
insert_contract(&mut alloc, &contract);
1818
}
1919

20+
if let Some(ref p2_config) = config.contracts.permit2 {
21+
let contract = contracts::permit2::build(p2_config, config.chain.chain_id);
22+
insert_contract(&mut alloc, &contract);
23+
}
24+
2025
Value::Object(alloc)
2126
}
2227

@@ -102,6 +107,7 @@ mod tests {
102107
address: address!("000000000000000000000000000000000000ad00"),
103108
owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
104109
}),
110+
permit2: None,
105111
},
106112
}
107113
}

bin/ev-deployer/src/init_template.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ chain_id = 0
1414
# [contracts.admin_proxy]
1515
# address = "0x000000000000000000000000000000000000Ad00"
1616
# owner = "0x..."
17+
18+
# Permit2: Uniswap canonical token approval manager.
19+
# [contracts.permit2]
20+
# address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"

0 commit comments

Comments
 (0)