From 35618c3645e86a2534d5ec1f364418b8fd1a2c90 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 16 Feb 2026 11:56:18 -0500 Subject: [PATCH 1/6] refactor: Introduce ILidoWithdrawalQueue interface in stETH management scripts for improved type safety and clarity --- .../AutomateStEthWithdrawals.s.sol | 19 +++++++++++++++---- .../ClaimStEthWithdrawals.s.sol | 18 ++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/script/operations/steth-management/AutomateStEthWithdrawals.s.sol b/script/operations/steth-management/AutomateStEthWithdrawals.s.sol index 42629ab1d..67f41edbc 100644 --- a/script/operations/steth-management/AutomateStEthWithdrawals.s.sol +++ b/script/operations/steth-management/AutomateStEthWithdrawals.s.sol @@ -5,7 +5,18 @@ import "forge-std/Script.sol"; import "forge-std/console2.sol"; import {Deployed} from "../../deploys/Deployed.s.sol"; import {EtherFiRestaker} from "../../../src/EtherFiRestaker.sol"; -import {ILido, ILidoWithdrawalQueue} from "../../../src/interfaces/ILiquifier.sol"; +import {ILido} from "../../../src/interfaces/ILiquifier.sol"; + + +interface ILidoWithdrawalQueue { + function getLastFinalizedRequestId() external view returns (uint256); + function getLastCheckpointIndex() external view returns (uint256); + function findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex) external view returns (uint256[] memory hintIds); + function prefinalize(uint256[] calldata _batches, uint256 _maxShareRate) external view returns (uint256 ethToLock, uint256 sharesToBurn); + function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable; + function FINALIZE_ROLE() external view returns (bytes32); + function getRoleMember(bytes32 _role, uint256 _index) external view returns (address); +} // Full withdrawal: // FULL_WITHDRAWAL=true forge script script/operations/steth-claim-withdrawals/AutomateStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv @@ -18,7 +29,7 @@ contract AutomateStEthWithdrawals is Script, Deployed { EtherFiRestaker constant etherFiRestaker = EtherFiRestaker(payable(ETHERFI_RESTAKER)); function run() external { - ILidoWithdrawalQueue lidoWithdrawalQueue = etherFiRestaker.lidoWithdrawalQueue(); + ILidoWithdrawalQueue lidoWithdrawalQueue = ILidoWithdrawalQueue(address(etherFiRestaker.lidoWithdrawalQueue())); ILido lido = etherFiRestaker.lido(); bool fullWithdrawal = vm.envOr("FULL_WITHDRAWAL", false); @@ -100,8 +111,8 @@ contract AutomateStEthWithdrawals is Script, Deployed { } // Get checkpoint hints - uint256 lastCheckpointIndex = lidoWithdrawalQueue.getLastCheckpointIndex(); - uint256[] memory hints = lidoWithdrawalQueue.findCheckpointHints(claimableIds, 1, lastCheckpointIndex); + uint256 lastCheckpointIndex = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).getLastCheckpointIndex(); + uint256[] memory hints = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).findCheckpointHints(claimableIds, 1, lastCheckpointIndex); // Log claim calldata bytes memory claimCalldata = abi.encodeWithSelector( diff --git a/script/operations/steth-management/ClaimStEthWithdrawals.s.sol b/script/operations/steth-management/ClaimStEthWithdrawals.s.sol index d3a804746..820b902a7 100644 --- a/script/operations/steth-management/ClaimStEthWithdrawals.s.sol +++ b/script/operations/steth-management/ClaimStEthWithdrawals.s.sol @@ -5,7 +5,13 @@ import "forge-std/Script.sol"; import "forge-std/console2.sol"; import {Deployed} from "../../deploys/Deployed.s.sol"; import {EtherFiRestaker} from "../../../src/EtherFiRestaker.sol"; -import {ILidoWithdrawalQueue} from "../../../src/interfaces/ILiquifier.sol"; +// import {ILidoWithdrawalQueue} from "../../../src/interfaces/ILiquifier.sol"; + +interface ILidoWithdrawalQueue { + function getLastFinalizedRequestId() external view returns (uint256); + function getLastCheckpointIndex() external view returns (uint256); + function findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex) external view returns (uint256[] memory hintIds); +} // forge script script/operations/steth-claim-withdrawals/ClaimStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv @@ -17,14 +23,14 @@ contract ClaimStEthWithdrawals is Script, Deployed { uint256 startId = 113785; // Set this to the first request you want to claim uint256 endId = 113863; // Set this to the last request you want to claim - ILidoWithdrawalQueue lidoWithdrawalQueue = etherFiRestaker.lidoWithdrawalQueue(); + ILidoWithdrawalQueue lidoWithdrawalQueue = ILidoWithdrawalQueue(address(etherFiRestaker.lidoWithdrawalQueue())); console2.log("LidoWithdrawalQueue:", address(lidoWithdrawalQueue)); // Cap endId to the last finalized request - uint256 lastFinalizedId = lidoWithdrawalQueue.getLastFinalizedRequestId(); + uint256 lastFinalizedId = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).getLastFinalizedRequestId(); console2.log("Last finalized request ID:", lastFinalizedId); - if (endId > lastFinalizedId) { + if (endId > lastFinalizedId) { console2.log("WARNING: endId", endId, "exceeds last finalized ID, capping to", lastFinalizedId); endId = lastFinalizedId; } @@ -40,10 +46,10 @@ contract ClaimStEthWithdrawals is Script, Deployed { } // Get checkpoint hints - uint256 lastCheckpointIndex = lidoWithdrawalQueue.getLastCheckpointIndex(); + uint256 lastCheckpointIndex = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).getLastCheckpointIndex(); console2.log("Last checkpoint index:", lastCheckpointIndex); - uint256[] memory hints = lidoWithdrawalQueue.findCheckpointHints(requestIds, 1, lastCheckpointIndex); + uint256[] memory hints = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).findCheckpointHints(requestIds, 1, lastCheckpointIndex); console2.log("Hints found for", hints.length, "requests"); for (uint256 i = 0; i < hints.length; i++) { From a73e0920ec8f126b0385ab4b86225352a1c432b6 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 16 Feb 2026 11:57:00 -0500 Subject: [PATCH 2/6] test: Update EtherFiRestaker tests to correctly assert pending redemptions and total pooled ether after withdrawals --- test/EtherFiRestaker.t.sol | 11 ++++++----- test/behaviour-tests/prelude.t.sol | 11 +++++++---- test/integration-tests/Withdraw.t.sol | 11 +++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol index 4d6c04892..f00110e16 100644 --- a/test/EtherFiRestaker.t.sol +++ b/test/EtherFiRestaker.t.sol @@ -62,18 +62,19 @@ contract EtherFiRestakerTest is TestSetup { uint256 lpBalance = address(liquidityPoolInstance).balance; uint256 currentEtherFiRestakerTotalPooledEther = etherFiRestakerInstance.getTotalPooledEther(); uint256 currentStEthBalance = stEth.balanceOf(address(etherFiRestakerInstance)); + uint256 initialPendingRedemption = etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)); uint256 amount = 10 ether; _deposit_stEth(amount); - assertEq(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), 0); + assertEq(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), initialPendingRedemption); vm.startPrank(owner); uint256 stEthBalance = stEth.balanceOf(address(etherFiRestakerInstance)) - currentStEthBalance; uint256[] memory reqIds = etherFiRestakerInstance.stEthRequestWithdrawal(stEthBalance); vm.stopPrank(); - assertApproxEqAbs(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), amount, 2 wei); + assertApproxEqAbs(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), initialPendingRedemption + amount, 2 wei); assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), currentEtherFiRestakerTotalPooledEther + amount, 2 wei); assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); @@ -87,7 +88,7 @@ contract EtherFiRestakerTest is TestSetup { etherFiRestakerInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate); vm.stopPrank(); - assertApproxEqAbs(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), amount, 2 wei); + assertApproxEqAbs(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), initialPendingRedemption + amount, 2 wei); assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), currentEtherFiRestakerTotalPooledEther + amount, 2 wei); assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); @@ -97,8 +98,8 @@ contract EtherFiRestakerTest is TestSetup { uint256[] memory hints = etherFiRestakerInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); - // the cycle completes - assertApproxEqAbs(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), 0, 2 wei); + // the cycle completes - only the newly requested amount should be redeemed, initial pending remains + assertApproxEqAbs(etherFiRestakerInstance.getAmountPendingForRedemption(address(stEth)), initialPendingRedemption, 2 wei); assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), currentEtherFiRestakerTotalPooledEther, 2 wei); assertApproxEqAbs(address(etherFiRestakerInstance).balance, 0, 2); assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); diff --git a/test/behaviour-tests/prelude.t.sol b/test/behaviour-tests/prelude.t.sol index f50b94321..8b8b3247f 100644 --- a/test/behaviour-tests/prelude.t.sol +++ b/test/behaviour-tests/prelude.t.sol @@ -628,13 +628,16 @@ contract PreludeTest is Test, ArrayTestHelper { vm.stopPrank(); } + // poke withdrawable funds into the restakedExecutionLayerGwei storage slot of the eigenpod + // Must be done before queueing to ensure the EigenPod has sufficient state. + // Use a large value to also cover any pre-existing queued withdrawals on mainnet. + address eigenpod = etherFiNodesManager.getEigenPod(uint256(pubkeyHash)); + vm.store(eigenpod, bytes32(uint256(52)) /*slot*/, bytes32(uint256(10000 ether / 1 gwei))); + vm.deal(eigenpod, 10000 ether); + vm.prank(eigenlayerAdmin); etherFiNodesManager.queueETHWithdrawal(uint256(pubkeyHash), 1 ether); - // poke some withdrawable funds into the restakedExecutionLayerGwei storage slot of the eigenpod - address eigenpod = etherFiNodesManager.getEigenPod(uint256(pubkeyHash)); - vm.store(eigenpod, bytes32(uint256(52)) /*slot*/, bytes32(uint256(50 ether / 1 gwei))); - uint256 startingBalance = address(liquidityPool).balance; vm.roll(block.number + (7200 * 15)); diff --git a/test/integration-tests/Withdraw.t.sol b/test/integration-tests/Withdraw.t.sol index a0dd28c21..0bf02a81a 100644 --- a/test/integration-tests/Withdraw.t.sol +++ b/test/integration-tests/Withdraw.t.sol @@ -42,6 +42,8 @@ contract WithdrawIntegrationTest is TestSetup, Deployed { // Ensure bucket limiter has enough capacity and is fully refilled etherFiRedemptionManagerInstance.setCapacity(3000 ether, ETH_ADDRESS); etherFiRedemptionManagerInstance.setRefillRatePerSecond(3000 ether, ETH_ADDRESS); + // On mainnet fork, lowWatermark (% of TVL) can be much larger than available liquidity + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(0, ETH_ADDRESS); vm.stopPrank(); // Warp time forward to ensure bucket is fully refilled @@ -84,6 +86,8 @@ contract WithdrawIntegrationTest is TestSetup, Deployed { // Ensure bucket limiter has enough capacity and is fully refilled etherFiRedemptionManagerInstance.setCapacity(3000 ether, ETH_ADDRESS); etherFiRedemptionManagerInstance.setRefillRatePerSecond(3000 ether, ETH_ADDRESS); + // On mainnet fork, lowWatermark (% of TVL) can be much larger than available liquidity + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(0, ETH_ADDRESS); vm.stopPrank(); // Warp time forward to ensure bucket is fully refilled @@ -124,6 +128,10 @@ contract WithdrawIntegrationTest is TestSetup, Deployed { } function test_Withdraw_EtherFiRedemptionManager_redeemWeEth() public { + // On mainnet fork, lowWatermark (% of TVL) can be much larger than available liquidity + vm.prank(OPERATING_TIMELOCK); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(0, ETH_ADDRESS); + vm.deal(alice, 100 ether); vm.startPrank(alice); liquidityPoolInstance.deposit{value: 10 ether}(); // to get eETH to generate weETH @@ -162,6 +170,9 @@ contract WithdrawIntegrationTest is TestSetup, Deployed { } function test_Withdraw_EtherFiRedemptionManager_redeemWeEthWithPermit() public { + // On mainnet fork, lowWatermark (% of TVL) can be much larger than available liquidity + vm.prank(OPERATING_TIMELOCK); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(0, ETH_ADDRESS); vm.deal(alice, 100 ether); vm.startPrank(alice); From 30693490dcb9a71816093290340e32f01ed0391e Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 16 Feb 2026 11:57:16 -0500 Subject: [PATCH 3/6] docs: Add CLAUDE.md for EtherFi Smart Contracts, detailing build/test instructions, project layout, architecture, key addresses, access control roles, test setup patterns, and EigenLayer integration --- CLAUDE.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..d5c472d03 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# EtherFi Smart Contracts + +## Build & Test + +```bash +forge build # compile +forge test --match-test # unit tests (no RPC needed) +forge test --match-test --fork-url $MAINNET_RPC_URL # mainnet fork tests +``` + +- Solidity 0.8.27, Foundry with 1500 optimizer runs +- Env vars: `MAINNET_RPC_URL`, `FORK_RPC_URL` (optional override), `VALIDATOR_DB`, `BEACON_NODE_URL` + +## Project Layout + +``` +src/ # Core contracts + EtherFiNode.sol # Per-validator-group contract, owns an EigenPod + EtherFiNodesManager.sol # Entry point for pod operations (0x8B71...6F) + LiquidityPool.sol # Main ETH pool (0x3088...16) + EtherFiRestaker.sol # Manages stETH restaking via EigenLayer (0x1B7a...Ff) + EtherFiRedemptionManager.sol # Instant redemptions with rate limiting (0xDadE...e0) + StakingManager.sol # Validator lifecycle + WeETH.sol / EETH.sol # Token contracts + eigenlayer-interfaces/ # EigenLayer interface definitions (no implementations) +test/ + TestSetup.sol # Base test with initializeRealisticFork() / initializeTestingFork() + behaviour-tests/ # PreludeTest - validator lifecycle on mainnet fork + integration-tests/ # Cross-contract integration tests on mainnet fork + fork-tests/ # Additional fork-based tests +script/ + operations/ # Operational tooling (Python + Solidity for Gnosis Safe txns) + deploys/Deployed.s.sol # All mainnet deployed addresses as constants +``` + +## Architecture + +- Validator pubkey -> `etherFiNodeFromPubkeyHash` -> EtherFiNode -> `getEigenPod()` -> EigenPod +- `calculateValidatorPubkeyHash`: `sha256(pubkey + bytes16(0))` +- Legacy validators use integer IDs; new validators use pubkey hashes. `etherfiNodeAddress(id)` resolves both via a heuristic on upper bits. +- UUPS proxy pattern throughout. Upgrades go through timelocks. + +## Key Addresses (Mainnet) + +| Role | Address | +|------|---------| +| OPERATING_TIMELOCK | `0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a` | +| UPGRADE_TIMELOCK | `0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761` | +| ROLE_REGISTRY | `0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9` | +| EIGENLAYER_DELEGATION_MANAGER | `0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A` | +| EIGENLAYER_POD_MANAGER | `0x91E677b07F7AF907ec9a428aafA9fc14a0d3A338` | +| LIDO_WITHDRAWAL_QUEUE | `0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1` | + +Full list in `script/deploys/Deployed.s.sol`. + +## Access Control Roles + +- `ETHERFI_NODES_MANAGER_POD_PROVER_ROLE` -> startCheckpoint, verifyCheckpointProofs +- `ETHERFI_NODES_MANAGER_EIGENLAYER_ADMIN_ROLE` -> queueETHWithdrawal, completeQueuedETHWithdrawals +- `ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE` -> setCapacity, setRefillRate, setLowWatermark, setExitFee +- `OPERATING_TIMELOCK` holds the redemption manager admin role + +## Test Setup Patterns + +Two fork modes in `TestSetup.sol`: +- `initializeRealisticFork(MAINNET_FORK)` — uses real mainnet contracts at their deployed addresses. Forks at **latest block** (no pinned block), so mainnet state drifts. +- `initializeTestingFork(MAINNET_FORK)` — deploys fresh contracts on a mainnet fork. + +`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`). + +## Mainnet Fork Test Decisions + +These are hard-won lessons. Follow them when writing or fixing fork tests: + +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. + +2. **EigenPod storage slot 52** = `withdrawableRestakedExecutionLayerGwei` (uint64, packed with `proofSubmitter` address in same slot). When poking this with `vm.store`: + - Set it BEFORE any `queueETHWithdrawal` / `completeQueuedETHWithdrawals` calls + - Use a large value (10000+ ETH in gwei) to cover pre-existing queued withdrawals from mainnet state + - Also `vm.deal` ETH to the EigenPod so it can actually transfer funds during withdrawal completion + - `completeQueuedETHWithdrawals` iterates ALL eligible queued withdrawals, not just the one you queued in the test + +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. + +4. **Rate limiter (BucketLimiter)** must also be configured in fork tests: `setCapacity()` + `setRefillRatePerSecond()` + `vm.warp(block.timestamp + 1)` to refill. + +## EigenLayer Integration + +- EigenPod key function selectors: `currentCheckpointTimestamp()` = `0x42ecff2a`, `lastCheckpointTimestamp()` = `0xee94d67c`, `activeValidatorCount()` = `0x2340e8d3` +- EigenPod storage slots used in tests: slot 52 = `withdrawableRestakedExecutionLayerGwei`, slot 57 = `activeValidatorCount` +- Beacon ETH strategy address: `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0` +- Withdrawal delay: `EIGENLAYER_WITHDRAWAL_DELAY_BLOCKS = 100800` blocks (~14 days) + +## Operations Tooling + +- Python scripts in `script/operations/` use `validator_utils.py` for shared DB/beacon utilities +- Solidity scripts in same dirs generate Gnosis Safe transactions +- DB tables: `etherfi_validators` (pubkey, id, phase, status, node_address), `MainnetValidators` (pubkey, eigen_pod_contract, etherfi_node_contract) +- Withdrawal credentials format: `0x01 + 22_zero_chars + 40_char_eigenpod_address` From a203a297937fdecd8562847edacf390408ec9c7c Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 16 Feb 2026 12:08:41 -0500 Subject: [PATCH 4/6] fix: Improve coverage report generation in GitHub Actions workflow by capturing full output and refining error messages --- .github/workflows/run-forge-tests.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run-forge-tests.yaml b/.github/workflows/run-forge-tests.yaml index c927aa0d6..931532d69 100644 --- a/.github/workflows/run-forge-tests.yaml +++ b/.github/workflows/run-forge-tests.yaml @@ -37,21 +37,21 @@ jobs: if: always() shell: bash run: | - set -o pipefail - if forge coverage --report summary --no-match-coverage '(script/|test/|src/helpers/|src/interfaces/|src/eigenlayer|src/libraries/|src/archive/)' --color never 2>&1 | tee full_output.txt | sed -n '/^[|╭╰+]/p' > coverage.txt; then + # Capture full output; || true so the step always continues + forge coverage --report summary \ + --no-match-coverage '(script/|test/|src/helpers/|src/interfaces/|src/eigenlayer|src/libraries/|src/archive/)' \ + --color never > full_output.txt 2>&1 || true + + # Extract coverage table lines + grep -E '^\|' full_output.txt > coverage.txt || true + + if [ -s coverage.txt ]; then echo "" >> coverage.txt echo "---" >> coverage.txt grep -E "^(Ran|Suite result:|Test result:)" full_output.txt >> coverage.txt || true else - echo "forge coverage failed; see logs above (tests step is the gate)." > coverage.txt + echo "Coverage report could not be generated. Check workflow logs for details." > coverage.txt fi - # Debug output - echo "=== DEBUG: coverage.txt contents (first 20 lines) ===" - head -20 coverage.txt || echo "(empty or missing)" - echo "=== DEBUG: coverage.txt size ===" - wc -l coverage.txt || echo "(file missing)" - echo "=== DEBUG: full_output.txt table lines ===" - grep -cE '^[|╭╰+]' full_output.txt || echo "0 lines match table pattern" env: MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} SCROLL_RPC_URL: ${{ secrets.SCROLL_RPC_URL }} From 3bbd8606b3b4122e66edfdb8fb0da05b3f5504bb Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 16 Feb 2026 12:35:25 -0500 Subject: [PATCH 5/6] feat: Implement _syncOracleReportState function to align admin's lastHandledReportRefSlot with oracle's lastPublishedReportRefSlot in integration tests --- test/integration-tests/Validator-Flows.t.sol | 22 ++++++++++++++++++++ test/integration-tests/Withdraw.t.sol | 21 ++++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/test/integration-tests/Validator-Flows.t.sol b/test/integration-tests/Validator-Flows.t.sol index 2901f426d..b97729de1 100644 --- a/test/integration-tests/Validator-Flows.t.sol +++ b/test/integration-tests/Validator-Flows.t.sol @@ -12,6 +12,28 @@ import "../../src/libraries/DepositDataRootGenerator.sol"; contract ValidatorFlowsIntegrationTest is TestSetup, Deployed { function setUp() public { initializeRealisticFork(MAINNET_FORK); + + // Handle any pending oracle report that hasn't been processed yet + _syncOracleReportState(); + } + + /// @dev Advances the admin's lastHandledReportRefSlot to match the oracle's lastPublishedReportRefSlot. + function _syncOracleReportState() internal { + uint32 lastPublished = etherFiOracleInstance.lastPublishedReportRefSlot(); + uint32 lastHandled = etherFiAdminInstance.lastHandledReportRefSlot(); + + if (lastPublished != lastHandled) { + uint32 lastPublishedBlock = etherFiOracleInstance.lastPublishedReportRefBlock(); + + // EtherFiAdmin slot 209 packs: lastHandledReportRefSlot (4B @ offset 0) + + // lastHandledReportRefBlock (4B @ offset 4) + other fields in higher bytes + bytes32 slot209 = vm.load(address(etherFiAdminInstance), bytes32(uint256(209))); + uint256 val = uint256(slot209); + val &= ~uint256(0xFFFFFFFFFFFFFFFF); // clear low 64 bits (both uint32 fields) + val |= uint256(lastPublished); + val |= uint256(lastPublishedBlock) << 32; + vm.store(address(etherFiAdminInstance), bytes32(uint256(209)), bytes32(val)); + } } function _toArray(IStakingManager.DepositData memory d) internal pure returns (IStakingManager.DepositData[] memory arr) { diff --git a/test/integration-tests/Withdraw.t.sol b/test/integration-tests/Withdraw.t.sol index 0bf02a81a..57219083a 100644 --- a/test/integration-tests/Withdraw.t.sol +++ b/test/integration-tests/Withdraw.t.sol @@ -19,20 +19,25 @@ contract WithdrawIntegrationTest is TestSetup, Deployed { _syncOracleReportState(); } - /// @dev Syncs the oracle's lastPublishedReportRefSlot with the admin's lastHandledReportRefSlot - /// This is necessary when forking mainnet where there may be a pending report + /// @dev Advances the admin's lastHandledReportRefSlot to match the oracle's lastPublishedReportRefSlot. + /// On mainnet fork there may be a published report the admin hasn't processed yet. + /// We advance the admin forward (not rewind the oracle) so that slotForNextReport() + /// returns the correct next slot and committee members can still submit new reports. function _syncOracleReportState() internal { uint32 lastPublished = etherFiOracleInstance.lastPublishedReportRefSlot(); uint32 lastHandled = etherFiAdminInstance.lastHandledReportRefSlot(); if (lastPublished != lastHandled) { - // Use the oracle's admin function to sync the state - // Get the oracle admin (owner in this case) - address oracleOwner = etherFiOracleInstance.owner(); uint32 lastPublishedBlock = etherFiOracleInstance.lastPublishedReportRefBlock(); - - vm.prank(oracleOwner); - etherFiOracleInstance.updateLastPublishedBlockStamps(lastHandled, lastPublishedBlock); + + // EtherFiAdmin slot 209 packs: lastHandledReportRefSlot (4B @ offset 0) + + // lastHandledReportRefBlock (4B @ offset 4) + other fields in higher bytes + bytes32 slot209 = vm.load(address(etherFiAdminInstance), bytes32(uint256(209))); + uint256 val = uint256(slot209); + val &= ~uint256(0xFFFFFFFFFFFFFFFF); // clear low 64 bits (both uint32 fields) + val |= uint256(lastPublished); + val |= uint256(lastPublishedBlock) << 32; + vm.store(address(etherFiAdminInstance), bytes32(uint256(209)), bytes32(val)); } } From adb96c9a6e167dd064db4e2a1eccff04df3030c0 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 16 Feb 2026 14:12:02 -0500 Subject: [PATCH 6/6] fix: Update import paths for Utils in deploy.s.sol and transactions.s.sol to ensure consistency --- script/upgrades/CrossPodApproval/deploy.s.sol | 2 +- script/upgrades/CrossPodApproval/transactions.s.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/upgrades/CrossPodApproval/deploy.s.sol b/script/upgrades/CrossPodApproval/deploy.s.sol index 19aa092f4..1fcb3901f 100644 --- a/script/upgrades/CrossPodApproval/deploy.s.sol +++ b/script/upgrades/CrossPodApproval/deploy.s.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import {LiquidityPool} from "../../../src/LiquidityPool.sol"; import {EtherFiNodesManager} from "../../../src/EtherFiNodesManager.sol"; import {Deployed} from "../../deploys/Deployed.s.sol"; -import {Utils, ICreate2Factory} from "../../utils/Utils.sol"; +import {Utils, ICreate2Factory} from "../../utils/utils.sol"; /** command: diff --git a/script/upgrades/CrossPodApproval/transactions.s.sol b/script/upgrades/CrossPodApproval/transactions.s.sol index e38d75420..0c5381fd4 100644 --- a/script/upgrades/CrossPodApproval/transactions.s.sol +++ b/script/upgrades/CrossPodApproval/transactions.s.sol @@ -12,7 +12,7 @@ import {EtherFiRateLimiter} from "../../../src/EtherFiRateLimiter.sol"; import {IEtherFiNodesManager} from "../../../src/interfaces/IEtherFiNodesManager.sol"; import {ContractCodeChecker} from "../../ContractCodeChecker.sol"; import {Deployed} from "../../deploys/Deployed.s.sol"; -import {Utils} from "../../utils/Utils.sol"; +import {Utils} from "../../utils/utils.sol"; import {IEigenPodTypes} from "../../../src/eigenlayer-interfaces/IEigenPod.sol"; // forge script script/upgrades/CrossPodApproval/transactions.s.sol:CrossPodApprovalScript --fork-url $MAINNET_RPC_URL -vvvv