|
| 1 | +# EtherFi Smart Contracts |
| 2 | + |
| 3 | +## Build & Test |
| 4 | + |
| 5 | +```bash |
| 6 | +forge build # compile |
| 7 | +forge test --match-test <name> # unit tests (no RPC needed) |
| 8 | +forge test --match-test <name> --fork-url $MAINNET_RPC_URL # mainnet fork tests |
| 9 | +``` |
| 10 | + |
| 11 | +- Solidity 0.8.27, Foundry with 1500 optimizer runs |
| 12 | +- Env vars: `MAINNET_RPC_URL`, `FORK_RPC_URL` (optional override), `VALIDATOR_DB`, `BEACON_NODE_URL` |
| 13 | + |
| 14 | +## Project Layout |
| 15 | + |
| 16 | +``` |
| 17 | +src/ # Core contracts |
| 18 | + EtherFiNode.sol # Per-validator-group contract, owns an EigenPod |
| 19 | + EtherFiNodesManager.sol # Entry point for pod operations (0x8B71...6F) |
| 20 | + LiquidityPool.sol # Main ETH pool (0x3088...16) |
| 21 | + EtherFiRestaker.sol # Manages stETH restaking via EigenLayer (0x1B7a...Ff) |
| 22 | + EtherFiRedemptionManager.sol # Instant redemptions with rate limiting (0xDadE...e0) |
| 23 | + StakingManager.sol # Validator lifecycle |
| 24 | + WeETH.sol / EETH.sol # Token contracts |
| 25 | + eigenlayer-interfaces/ # EigenLayer interface definitions (no implementations) |
| 26 | +test/ |
| 27 | + TestSetup.sol # Base test with initializeRealisticFork() / initializeTestingFork() |
| 28 | + behaviour-tests/ # PreludeTest - validator lifecycle on mainnet fork |
| 29 | + integration-tests/ # Cross-contract integration tests on mainnet fork |
| 30 | + fork-tests/ # Additional fork-based tests |
| 31 | +script/ |
| 32 | + operations/ # Operational tooling (Python + Solidity for Gnosis Safe txns) |
| 33 | + deploys/Deployed.s.sol # All mainnet deployed addresses as constants |
| 34 | +``` |
| 35 | + |
| 36 | +## Architecture |
| 37 | + |
| 38 | +- Validator pubkey -> `etherFiNodeFromPubkeyHash` -> EtherFiNode -> `getEigenPod()` -> EigenPod |
| 39 | +- `calculateValidatorPubkeyHash`: `sha256(pubkey + bytes16(0))` |
| 40 | +- Legacy validators use integer IDs; new validators use pubkey hashes. `etherfiNodeAddress(id)` resolves both via a heuristic on upper bits. |
| 41 | +- UUPS proxy pattern throughout. Upgrades go through timelocks. |
| 42 | + |
| 43 | +## Key Addresses (Mainnet) |
| 44 | + |
| 45 | +| Role | Address | |
| 46 | +|------|---------| |
| 47 | +| OPERATING_TIMELOCK | `0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a` | |
| 48 | +| UPGRADE_TIMELOCK | `0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761` | |
| 49 | +| ROLE_REGISTRY | `0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9` | |
| 50 | +| EIGENLAYER_DELEGATION_MANAGER | `0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A` | |
| 51 | +| EIGENLAYER_POD_MANAGER | `0x91E677b07F7AF907ec9a428aafA9fc14a0d3A338` | |
| 52 | +| LIDO_WITHDRAWAL_QUEUE | `0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1` | |
| 53 | + |
| 54 | +Full list in `script/deploys/Deployed.s.sol`. |
| 55 | + |
| 56 | +## Access Control Roles |
| 57 | + |
| 58 | +- `ETHERFI_NODES_MANAGER_POD_PROVER_ROLE` -> startCheckpoint, verifyCheckpointProofs |
| 59 | +- `ETHERFI_NODES_MANAGER_EIGENLAYER_ADMIN_ROLE` -> queueETHWithdrawal, completeQueuedETHWithdrawals |
| 60 | +- `ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE` -> setCapacity, setRefillRate, setLowWatermark, setExitFee |
| 61 | +- `OPERATING_TIMELOCK` holds the redemption manager admin role |
| 62 | + |
| 63 | +## Test Setup Patterns |
| 64 | + |
| 65 | +Two fork modes in `TestSetup.sol`: |
| 66 | +- `initializeRealisticFork(MAINNET_FORK)` — uses real mainnet contracts at their deployed addresses. Forks at **latest block** (no pinned block), so mainnet state drifts. |
| 67 | +- `initializeTestingFork(MAINNET_FORK)` — deploys fresh contracts on a mainnet fork. |
| 68 | + |
| 69 | +`PreludeTest` (behaviour-tests) has its own setup: forks mainnet, upgrades contracts in-place, deploys fresh RateLimiter, grants roles to test addresses (`admin`, `eigenlayerAdmin`, `podProver`, `elExiter`). |
| 70 | + |
| 71 | +## Mainnet Fork Test Decisions |
| 72 | + |
| 73 | +These are hard-won lessons. Follow them when writing or fixing fork tests: |
| 74 | + |
| 75 | +1. **Never assume zero baselines.** Mainnet contracts have live state (pending withdrawals, balances, queued operations). Always capture initial values and assert deltas relative to them. |
| 76 | + |
| 77 | +2. **EigenPod storage slot 52** = `withdrawableRestakedExecutionLayerGwei` (uint64, packed with `proofSubmitter` address in same slot). When poking this with `vm.store`: |
| 78 | + - Set it BEFORE any `queueETHWithdrawal` / `completeQueuedETHWithdrawals` calls |
| 79 | + - Use a large value (10000+ ETH in gwei) to cover pre-existing queued withdrawals from mainnet state |
| 80 | + - Also `vm.deal` ETH to the EigenPod so it can actually transfer funds during withdrawal completion |
| 81 | + - `completeQueuedETHWithdrawals` iterates ALL eligible queued withdrawals, not just the one you queued in the test |
| 82 | + |
| 83 | +3. **RedemptionManager lowWatermark blocks redemptions on fork.** `lowWatermarkInETH = totalPooledEther * lowWatermarkInBpsOfTvl / 10000`. On mainnet, TVL is millions of ETH, so even a 1% watermark = tens of thousands ETH. Test deposits of a few thousand ETH can never exceed this. Fix: `setLowWatermarkInBpsOfTvl(0, token)` via `OPERATING_TIMELOCK` at start of test. |
| 84 | + |
| 85 | +4. **Rate limiter (BucketLimiter)** must also be configured in fork tests: `setCapacity()` + `setRefillRatePerSecond()` + `vm.warp(block.timestamp + 1)` to refill. |
| 86 | + |
| 87 | +## EigenLayer Integration |
| 88 | + |
| 89 | +- EigenPod key function selectors: `currentCheckpointTimestamp()` = `0x42ecff2a`, `lastCheckpointTimestamp()` = `0xee94d67c`, `activeValidatorCount()` = `0x2340e8d3` |
| 90 | +- EigenPod storage slots used in tests: slot 52 = `withdrawableRestakedExecutionLayerGwei`, slot 57 = `activeValidatorCount` |
| 91 | +- Beacon ETH strategy address: `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0` |
| 92 | +- Withdrawal delay: `EIGENLAYER_WITHDRAWAL_DELAY_BLOCKS = 100800` blocks (~14 days) |
| 93 | + |
| 94 | +## Operations Tooling |
| 95 | + |
| 96 | +- Python scripts in `script/operations/` use `validator_utils.py` for shared DB/beacon utilities |
| 97 | +- Solidity scripts in same dirs generate Gnosis Safe transactions |
| 98 | +- DB tables: `etherfi_validators` (pubkey, id, phase, status, node_address), `MainnetValidators` (pubkey, eigen_pod_contract, etherfi_node_contract) |
| 99 | +- Withdrawal credentials format: `0x01 + 22_zero_chars + 40_char_eigenpod_address` |
0 commit comments