Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
89eccaa
feat: add ev-deployer CLI for genesis contract allocation
randygrok Mar 13, 2026
0c8f54e
test: add bytecode verification tests for ev-deployer contracts
randygrok Mar 13, 2026
2ba2b80
docs: add ev-deployer README with config and usage guide
randygrok Mar 13, 2026
a540858
fix(ci): serialize bytecode verification tests to avoid solc race con…
randygrok Mar 13, 2026
b9e2670
style: apply cargo fmt to ev-deployer
randygrok Mar 13, 2026
18ed817
ci(ev-deployer): split workflow into separate bytecode and unit test …
randygrok Mar 13, 2026
f7d0e71
style: fix fmt and clippy lint errors in ev-deployer
randygrok Mar 16, 2026
46ea9a6
feat(ev-deployer): add MerkleTreeHook contract with immutable bytecod…
randygrok Mar 18, 2026
e5f4eb9
Merge branch 'main' into ev-deployer-part1-core
randygrok Mar 18, 2026
be1b241
Merge remote-tracking branch 'origin/main' into ev-deployer-part1-core
randygrok Mar 19, 2026
7e19222
ci(ev-deployer): add e2e genesis test to CI workflow
randygrok Mar 19, 2026
e9fa70e
Merge remote-tracking branch 'origin/ev-deployer-part1-core' into ev-…
randygrok Mar 19, 2026
946026d
ci(ev-deployer): install soldeer deps before bytecode verification
randygrok Mar 19, 2026
9dd5011
test(ev-deployer): add MerkleTreeHook verification to e2e genesis test
randygrok Mar 19, 2026
217be0c
fix(ev-deployer): escape brackets in doc comments to fix rustdoc
randygrok Mar 19, 2026
67151c3
feat(ev-deployer): add Permit2 contract support
randygrok Mar 19, 2026
56548ec
docs(ev-deployer): add comment explaining canonical Permit2 address
randygrok Mar 19, 2026
fa8a428
merge: resolve e2e test conflicts with ev-deployer-merkle-tree-hook
randygrok Mar 19, 2026
bfa5a23
style(ev-deployer): fix fmt, clippy and rustdoc warnings in permit2
randygrok Mar 19, 2026
4da01ea
feat(ev-deployer): add Mailbox, NoopIsm, and ProtocolFee genesis cont…
randygrok Mar 19, 2026
3faa629
fix(ev-deployer): regenerate Mailbox and ProtocolFee bytecodes from c…
randygrok Mar 19, 2026
eb413d1
merge: resolve conflicts with ev-deployer-merkle-tree-hook (mailbox, …
jgimeno Mar 19, 2026
aeffc0d
fix(ev-deployer): address PR review feedback
jgimeno Mar 19, 2026
a2d194e
merge(ev-deployer): integrate ev-deployer-part1-core duplicate addres…
jgimeno Mar 19, 2026
e8a39f8
refactor(ev-deployer): remove FeeVault contract from part 1
randygrok Mar 24, 2026
089ef22
refactor(ev-deployer): remove AdminProxy contract from part 1
randygrok Mar 24, 2026
6b85563
Revert "refactor(ev-deployer): remove AdminProxy contract from part 1"
randygrok Mar 24, 2026
93b3eaa
fix(ev-deployer): make [contracts] section optional in config
randygrok Mar 24, 2026
70111fd
feat(ev-deployer): add init command to generate starter config
randygrok Mar 24, 2026
fa0e71f
fix(ev-deployer): clean up command ordering and stale fee_vault refer…
randygrok Mar 24, 2026
1acd3c8
docs(ev-deployer): document init command in README
randygrok Mar 24, 2026
cb838e8
merge(ev-deployer): integrate ev-deployer-part1-core keeping all cont…
randygrok Mar 24, 2026
ee68354
fix(ev-deployer): remove extra blank lines from merge to pass rustfmt
randygrok Mar 24, 2026
ef5ac9e
docs(ev-deployer): document all supported contracts in README and ini…
randygrok Mar 24, 2026
65bbf9e
fix(ev-deployer): normalize alloc keys for collision detection
randygrok Mar 24, 2026
08c9eb4
style(ev-deployer): fix fmt and clippy lint in genesis.rs
randygrok Mar 25, 2026
46b75bf
Merge remote-tracking branch 'origin/ev-deployer-part1-core' into ev-…
randygrok Mar 25, 2026
e365cfe
merge: resolve e2e test conflict with ev-deployer-merkle-tree-hook (p…
randygrok Mar 25, 2026
7f9e238
docs(ev-deployer): add Permit2 to init template and README
randygrok Mar 25, 2026
b1e5ae3
fix(ev-deployer): use case-insensitive grep in e2e genesis address ch…
randygrok Mar 25, 2026
5d2be71
refactor(ev-deployer): remove Hyperlane contracts, keep only AdminPro…
randygrok Mar 27, 2026
c32b633
merge: integrate main into ev-deployer-part3-permit2
randygrok Mar 27, 2026
04beb6b
refactor(contracts): remove Hyperlane dependency from FeeVault
randygrok Mar 27, 2026
c6e679b
style(contracts): fix forge fmt formatting
randygrok Mar 27, 2026
cae3723
docs: clarify FeeVault is optional, document when to use it vs plain …
randygrok Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .claude/skills/contracts.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
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.
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.
---

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

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

## Key Files

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

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

## Connection to Rust Code
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/lib/permit2"]
path = contracts/lib/permit2
url = https://github.com/Uniswap/permit2
25 changes: 12 additions & 13 deletions bin/ev-deployer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,6 @@ The binary is output to `target/release/ev-deployer`.

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.

```toml
[chain]
chain_id = 1234

[contracts.admin_proxy]
address = "0x000000000000000000000000000000000000Ad00"
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
```

### Config reference

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

#### `[contracts.permit2]`

| Field | Type | Description |
|-----------|---------|----------------------------------------------------------|
| `address` | address | Address to deploy at (canonical: `0x000000000022D473...`) |

Comment on lines +32 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the full canonical Permit2 address in the config reference.

Line 36 truncates the canonical address with ..., which is error-prone for copy/paste in config docs.

📝 Proposed doc fix
-| `address` | address | Address to deploy at (canonical: `0x000000000022D473...`) |
+| `address` | address | Address to deploy at (canonical: `0x000000000022D473030F116dDEE9F6B43aC78BA3`) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-deployer/README.md` around lines 32 - 37, Replace the truncated
canonical Permit2 address in the README section `[contracts.permit2]` (the
`address` field description) with the full canonical Permit2 address; update the
description string so it contains the complete address (e.g.,
0x000000000022D473030F116dDEE9F6B43aC78BA3) to allow accurate copy/paste for
configs and avoid the `...` truncation.

## Usage

### Generate a starter config
Expand Down Expand Up @@ -88,7 +85,8 @@ Output:

```json
{
"admin_proxy": "0x000000000000000000000000000000000000Ad00"
"admin_proxy": "0x000000000000000000000000000000000000Ad00",
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
}
```

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

## Contracts

| Contract | Description |
|----------------|-----------------------------------------------------|
| `admin_proxy` | Proxy contract with owner-based access control |
| Contract | Description |
|---------------|----------------------------------------------------|
| `admin_proxy` | Proxy contract with owner-based access control |
| `permit2` | Uniswap canonical token approval manager |

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

Expand Down
5 changes: 5 additions & 0 deletions bin/ev-deployer/examples/devnet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ chain_id = 1234
[contracts.admin_proxy]
address = "0x000000000000000000000000000000000000Ad00"
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"

[contracts.permit2]
# Canonical Uniswap Permit2 address (same on all chains via CREATE2).
# Using it here so frontends, SDKs and routers work out of the box.
address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
9 changes: 9 additions & 0 deletions bin/ev-deployer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub(crate) struct ChainConfig {
pub(crate) struct ContractsConfig {
/// `AdminProxy` contract config (optional).
pub admin_proxy: Option<AdminProxyConfig>,
/// `Permit2` contract config (optional).
pub permit2: Option<Permit2Config>,
}

/// `AdminProxy` configuration.
Expand All @@ -39,6 +41,13 @@ pub(crate) struct AdminProxyConfig {
pub owner: Address,
}

/// `Permit2` configuration (Uniswap token approval manager).
#[derive(Debug, Deserialize)]
pub(crate) struct Permit2Config {
/// Address to deploy at.
pub address: Address,
}
Comment on lines +44 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate contracts.permit2.address is non-zero.

Right now a zero Permit2 address is accepted, which can produce an invalid deployment target without early feedback.

Suggested fix
 fn validate(&self) -> eyre::Result<()> {
     if let Some(ref ap) = self.contracts.admin_proxy {
         eyre::ensure!(
             !ap.owner.is_zero(),
             "admin_proxy.owner must not be the zero address"
         );
     }
+
+    if let Some(ref p2) = self.contracts.permit2 {
+        eyre::ensure!(
+            !p2.address.is_zero(),
+            "permit2.address must not be the zero address"
+        );
+    }
 
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-deployer/src/config.rs` around lines 44 - 49, The Permit2Config
currently accepts a zero address; add a runtime validation to reject
Address::zero() for Permit2 by implementing a validation method (e.g.,
Permit2Config::validate) or a TryFrom/try_into conversion that is called after
deserialization and returns an error if self.address == Address::zero(), and
ensure whatever config-loading code invokes this validation so parsing fails
early with a clear error when contracts.permit2.address is zero.


impl DeployConfig {
/// Load and validate config from a TOML file.
pub(crate) fn load(path: &Path) -> eyre::Result<Self> {
Expand Down
76 changes: 76 additions & 0 deletions bin/ev-deployer/src/contracts/immutables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Bytecode patching for Solidity immutable variables.
//!
//! Solidity `immutable` values are embedded in the **runtime bytecode** by the
//! compiler, not in storage. When compiling with placeholder values (e.g.
//! `address(0)`, `uint32(0)`), the compiler leaves zero-filled regions at known
//! byte offsets. This module replaces those regions with the actual values from
//! the deploy config at genesis-generation time.

use alloy_primitives::{B256, U256};

/// A single immutable reference inside a bytecode blob.
#[derive(Debug, Clone, Copy)]
pub(crate) struct ImmutableRef {
/// Byte offset into the **runtime** bytecode.
pub start: usize,
/// Number of bytes (always 32 for EVM words).
pub length: usize,
}

/// Patch a mutable bytecode slice, writing `value` at every listed offset.
///
/// # Panics
///
/// Panics if any reference extends past the end of `bytecode`.
pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) {
for r in refs {
assert!(
r.start + r.length <= bytecode.len(),
"immutable ref out of bounds: start={} length={} bytecode_len={}",
r.start,
r.length,
bytecode.len()
);
bytecode[r.start..r.start + r.length].copy_from_slice(value);
}
Comment on lines +25 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Enforce immutable word-size invariant explicitly.

patch_bytes accepts arbitrary length, but copy_from_slice(value) requires exactly 32 bytes. Add an explicit check so failures are deterministic and self-explanatory.

Suggested fix
 pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) {
     for r in refs {
+        assert!(
+            r.length == value.len(),
+            "immutable ref length mismatch: expected={} got={} start={}",
+            value.len(),
+            r.length,
+            r.start
+        );
         assert!(
             r.start + r.length <= bytecode.len(),
             "immutable ref out of bounds: start={} length={} bytecode_len={}",
             r.start,
             r.length,
             bytecode.len()
         );
         bytecode[r.start..r.start + r.length].copy_from_slice(value);
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) {
for r in refs {
assert!(
r.start + r.length <= bytecode.len(),
"immutable ref out of bounds: start={} length={} bytecode_len={}",
r.start,
r.length,
bytecode.len()
);
bytecode[r.start..r.start + r.length].copy_from_slice(value);
}
pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) {
for r in refs {
assert!(
r.length == value.len(),
"immutable ref length mismatch: expected={} got={} start={}",
value.len(),
r.length,
r.start
);
assert!(
r.start + r.length <= bytecode.len(),
"immutable ref out of bounds: start={} length={} bytecode_len={}",
r.start,
r.length,
bytecode.len()
);
bytecode[r.start..r.start + r.length].copy_from_slice(value);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-deployer/src/contracts/immutables.rs` around lines 25 - 35, The
patch_bytes function currently assumes each ImmutableRef length equals the
32-byte value but doesn't enforce it, causing copy_from_slice to panic with
unclear errors; add an explicit check inside the loop that r.length ==
value.len() (or assert_eq!(r.length, 32, "...")) before the bounds assert/copy
so failures are deterministic and self-explanatory, referencing the ImmutableRef
fields (r.start, r.length) and value length in the assertion message.

}

/// Convenience: patch with an ABI-encoded `uint256`.
pub(crate) fn patch_u256(bytecode: &mut [u8], refs: &[ImmutableRef], val: U256) {
let word = B256::from(val);
patch_bytes(bytecode, refs, &word.0);
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn patch_single_ref() {
let mut bytecode = vec![0u8; 64];
let refs = [ImmutableRef {
start: 10,
length: 32,
}];
let value = B256::from(U256::from(42u64));
patch_bytes(&mut bytecode, &refs, &value.0);

assert_eq!(bytecode[41], 42);
// bytes before are untouched
assert_eq!(bytecode[9], 0);
// bytes after are untouched
assert_eq!(bytecode[42], 0);
}

#[test]
#[should_panic(expected = "immutable ref out of bounds")]
fn patch_out_of_bounds_panics() {
let mut bytecode = vec![0u8; 16];
let refs = [ImmutableRef {
start: 0,
length: 32,
}];
let value = [0u8; 32];
patch_bytes(&mut bytecode, &refs, &value);
}
}
2 changes: 2 additions & 0 deletions bin/ev-deployer/src/contracts/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Contract bytecode and storage encoding.

pub(crate) mod admin_proxy;
pub(crate) mod immutables;
pub(crate) mod permit2;

use alloy_primitives::{Address, Bytes, B256};
use std::collections::BTreeMap;
Expand Down
233 changes: 233 additions & 0 deletions bin/ev-deployer/src/contracts/permit2.rs

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions bin/ev-deployer/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub(crate) fn build_alloc(config: &DeployConfig) -> Value {
insert_contract(&mut alloc, &contract);
}

if let Some(ref p2_config) = config.contracts.permit2 {
let contract = contracts::permit2::build(p2_config, config.chain.chain_id);
insert_contract(&mut alloc, &contract);
}
Comment on lines +20 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent silent alloc overwrite when two configured contracts share one address.

At Line 20-23, adding Permit2 means two configured contracts can now collide. insert_contract uses Map::insert, so one entry is silently replaced instead of failing.

🛠️ Proposed fix (fail on duplicate deploy-config addresses)
-pub(crate) fn build_alloc(config: &DeployConfig) -> Value {
+pub(crate) fn build_alloc(config: &DeployConfig) -> eyre::Result<Value> {
     let mut alloc = Map::new();

     if let Some(ref ap_config) = config.contracts.admin_proxy {
         let contract = contracts::admin_proxy::build(ap_config);
-        insert_contract(&mut alloc, &contract);
+        insert_contract(&mut alloc, &contract)?;
     }

     if let Some(ref p2_config) = config.contracts.permit2 {
         let contract = contracts::permit2::build(p2_config, config.chain.chain_id);
-        insert_contract(&mut alloc, &contract);
+        insert_contract(&mut alloc, &contract)?;
     }

-    Value::Object(alloc)
+    Ok(Value::Object(alloc))
 }
@@
-    let alloc = build_alloc(config);
+    let alloc = build_alloc(config)?;
@@
-fn insert_contract(alloc: &mut Map<String, Value>, contract: &GenesisContract) {
+fn insert_contract(alloc: &mut Map<String, Value>, contract: &GenesisContract) -> eyre::Result<()> {
     let addr_key = normalize_addr(&format!("{}", contract.address));
@@
-    alloc.insert(addr_key, Value::Object(entry));
+    if alloc.insert(addr_key.clone(), Value::Object(entry)).is_some() {
+        eyre::bail!("duplicate deploy address in config: {addr_key}");
+    }
+    Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-deployer/src/genesis.rs` around lines 20 - 23, The insertion of the
Permit2 contract can silently overwrite an existing entry because
insert_contract uses Map::insert; update insert_contract (or the call site) to
detect duplicate deploy-config addresses and fail instead of replacing: before
calling insert_contract (or within insert_contract), check whether the
allocation map already contains the contract's address (use the same unique
identifier produced by contracts::permit2::build, e.g., the contract.address or
contract.id), and if it does return an error or panic with a clear message
indicating a duplicate configured contract address (include the conflicting
address and the two contract identifiers); alternatively use Map::entry and on
Occupied(_) return Err to prevent the silent overwrite.


Value::Object(alloc)
}

Expand Down Expand Up @@ -102,6 +107,7 @@ mod tests {
address: address!("000000000000000000000000000000000000ad00"),
owner: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
}),
permit2: None,
},
}
}
Expand Down
4 changes: 4 additions & 0 deletions bin/ev-deployer/src/init_template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ chain_id = 0
# [contracts.admin_proxy]
# address = "0x000000000000000000000000000000000000Ad00"
# owner = "0x..."

# Permit2: Uniswap canonical token approval manager.
# [contracts.permit2]
# address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
6 changes: 6 additions & 0 deletions bin/ev-deployer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ fn main() -> eyre::Result<()> {
.as_ref()
.map(|c| c.address)
.ok_or_else(|| eyre::eyre!("admin_proxy not configured"))?,
"permit2" => cfg
.contracts
.permit2
.as_ref()
.map(|c| c.address)
.ok_or_else(|| eyre::eyre!("permit2 not configured"))?,
other => eyre::bail!("unknown contract: {other}"),
};

Expand Down
7 changes: 7 additions & 0 deletions bin/ev-deployer/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,12 @@ pub(crate) fn build_manifest(config: &DeployConfig) -> Value {
);
}

if let Some(ref p2) = config.contracts.permit2 {
manifest.insert(
"permit2".to_string(),
Value::String(format!("{}", p2.address)),
);
}

Value::Object(manifest)
}
26 changes: 24 additions & 2 deletions bin/ev-deployer/tests/e2e_genesis.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ echo "=== Generating genesis with ev-deployer ==="
echo "Genesis written to $GENESIS"

# Quick sanity: address should be in the alloc
grep -q "000000000000000000000000000000000000Ad00" "$GENESIS" \
grep -qi "000000000000000000000000000000000000Ad00" "$GENESIS" \
|| fail "AdminProxy address not found in genesis"
grep -qi "000000000022D473030F116dDEE9F6B43aC78BA3" "$GENESIS" \
|| fail "Permit2 address not found in genesis"

pass "genesis contains AdminProxy address"
pass "genesis contains all contract addresses"

# ── Step 3: Start ev-reth ────────────────────────────────

Expand Down Expand Up @@ -125,6 +127,26 @@ expected_owner_slot="0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cff
|| fail "AdminProxy slot 0 (owner) mismatch: got $admin_slot0, expected $expected_owner_slot"
pass "AdminProxy owner slot 0 = $ADMIN_OWNER"

# ── Step 5: Verify Permit2 ──────────────────────────────

PERMIT2="0x000000000022D473030F116dDEE9F6B43aC78BA3"

echo "=== Verifying Permit2 at $PERMIT2 ==="

# Check code is present
p2_code=$(rpc_call "eth_getCode" "[\"$PERMIT2\", \"latest\"]")
[[ "$p2_code" != "0x" && "$p2_code" != "0x0" && ${#p2_code} -gt 10 ]] \
|| fail "Permit2 has no bytecode (got: $p2_code)"
pass "Permit2 has bytecode (${#p2_code} hex chars)"

# Call DOMAIN_SEPARATOR() — selector 0x3644e515
# Should return the cached domain separator matching chain_id=1234 and the contract address
p2_domain_sep=$(rpc_call "eth_call" "[{\"to\":\"$PERMIT2\",\"data\":\"0x3644e515\"}, \"latest\"]")
expected_domain_sep="0x6cda538cafce36292a6ef27740629597f85f6716f5694d26d5c59fc1d07cfd95"
[[ "$(echo "$p2_domain_sep" | tr '[:upper:]' '[:lower:]')" == "$(echo "$expected_domain_sep" | tr '[:upper:]' '[:lower:]')" ]] \
|| fail "Permit2 DOMAIN_SEPARATOR() mismatch: got $p2_domain_sep, expected $expected_domain_sep"
pass "Permit2 DOMAIN_SEPARATOR() correct for chain_id=1234"

# ── Done ─────────────────────────────────────────────────

echo ""
Expand Down
Loading
Loading