Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ interface ILidoWithdrawalQueue {
}

// Full withdrawal:
// FULL_WITHDRAWAL=true forge script script/operations/steth-claim-withdrawals/AutomateStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv
// FULL_WITHDRAWAL=true forge script script/operations/steth-management/AutomateStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv
//
// Partial withdrawal (amount in ether):
// AMOUNT=100 forge script script/operations/steth-claim-withdrawals/AutomateStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv
// AMOUNT=100 forge script script/operations/steth-management/AutomateStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv

contract AutomateStEthWithdrawals is Script, Deployed {

Expand Down
102 changes: 66 additions & 36 deletions script/operations/steth-management/ClaimStEthWithdrawals.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,75 @@ 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
// forge script script/operations/steth-management/ClaimStEthWithdrawals.s.sol --fork-url $MAINNET_RPC_URL -vvvv

contract ClaimStEthWithdrawals is Script, Deployed {

EtherFiRestaker constant etherFiRestaker = EtherFiRestaker(payable(ETHERFI_RESTAKER));

function run() external {
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 = ILidoWithdrawalQueue(address(etherFiRestaker.lidoWithdrawalQueue()));
console2.log("LidoWithdrawalQueue:", address(lidoWithdrawalQueue));
console2.log("EtherFiRestaker: ", address(etherFiRestaker));
console2.log("LidoWithdrawalQueue: ", address(lidoWithdrawalQueue));

// 1. Fetch all withdrawal request IDs owned by the restaker
uint256[] memory allRequestIds = lidoWithdrawalQueue.getWithdrawalRequests(address(etherFiRestaker));
console2.log("Total pending requests for restaker:", allRequestIds.length);

if (allRequestIds.length == 0) {
console2.log("No pending withdrawal requests found. Nothing to claim.");
return;
}

// 2. Get statuses for all requests
ILidoWithdrawalQueue.WithdrawalRequestStatus[] memory statuses =
lidoWithdrawalQueue.getWithdrawalStatus(allRequestIds);

// Cap endId to the last finalized request
uint256 lastFinalizedId = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).getLastFinalizedRequestId();
console2.log("Last finalized request ID:", lastFinalizedId);
// 3. Filter to finalized & unclaimed requests
uint256 claimableCount = 0;
for (uint256 i = 0; i < statuses.length; i++) {
if (statuses[i].isFinalized && !statuses[i].isClaimed) {
claimableCount++;
}
}

console2.log("Claimable (finalized & unclaimed):", claimableCount);

if (claimableCount == 0) {
console2.log("No claimable requests at this time. Nothing to claim.");
return;
}

if (endId > lastFinalizedId) {
console2.log("WARNING: endId", endId, "exceeds last finalized ID, capping to", lastFinalizedId);
endId = lastFinalizedId;
uint256[] memory requestIds = new uint256[](claimableCount);
uint256 idx = 0;
Comment thread
pankajjagtapp marked this conversation as resolved.
for (uint256 i = 0; i < allRequestIds.length; i++) {
if (statuses[i].isFinalized && !statuses[i].isClaimed) {
requestIds[idx] = allRequestIds[i];
idx++;
}
}
require(startId <= endId, "No finalized requests in range");

uint256 count = endId - startId + 1;
console2.log("Claiming", count, "requests:", startId);
console2.log(" to", endId);
_sortAscending(requestIds);

uint256[] memory requestIds = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
requestIds[i] = startId + i;
console2.log("Claiming request IDs:");
for (uint256 i = 0; i < requestIds.length; i++) {
console2.log(" ", requestIds[i]);
}

// Get checkpoint hints
uint256 lastCheckpointIndex = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).getLastCheckpointIndex();
// 4. Get checkpoint hints for the claimable requests
uint256 lastCheckpointIndex = lidoWithdrawalQueue.getLastCheckpointIndex();
console2.log("Last checkpoint index:", lastCheckpointIndex);

uint256[] memory hints = ILidoWithdrawalQueue(address(lidoWithdrawalQueue)).findCheckpointHints(requestIds, 1, lastCheckpointIndex);
uint256[] memory hints = lidoWithdrawalQueue.findCheckpointHints(requestIds, 1, lastCheckpointIndex);

console2.log("Hints found for", hints.length, "requests");
console2.log("Hints found for", hints.length, "requests:");
for (uint256 i = 0; i < hints.length; i++) {
console2.log(" requestId:", requestIds[i], "hint:", hints[i]);
}

// Encode the calldata
// 5. Encode calldata for multisig / safe submission
bytes memory callData = abi.encodeWithSelector(
EtherFiRestaker.stEthClaimWithdrawals.selector,
requestIds,
Expand All @@ -68,16 +85,29 @@ contract ClaimStEthWithdrawals is Script, Deployed {
console2.log("Target:", address(etherFiRestaker));
console2.logBytes(callData);

// Simulate the transaction on fork as operating admin
// 6. Simulate on fork as operating admin
console2.log("");
console2.log("=== Simulating on fork ===");
uint256 liquidityPoolbalanceBefore = LIQUIDITY_POOL.balance;
console2.log("LiquidityPool balance before:", uint256(liquidityPoolbalanceBefore) / 1e18);
uint256 lpBalanceBefore = LIQUIDITY_POOL.balance;
console2.log("LiquidityPool balance before:", lpBalanceBefore / 1e18);
vm.prank(ETHERFI_OPERATING_ADMIN);
etherFiRestaker.stEthClaimWithdrawals(requestIds, hints);
uint256 liquidityPoolbalanceAfter = LIQUIDITY_POOL.balance;
console2.log("LiquidityPool balance after:", uint256(liquidityPoolbalanceAfter) / 1e18);
console2.log("ETH claimed:", (liquidityPoolbalanceAfter - liquidityPoolbalanceBefore) / 1e18);
uint256 lpBalanceAfter = LIQUIDITY_POOL.balance;
console2.log("LiquidityPool balance after:", lpBalanceAfter / 1e18);
console2.log("ETH claimed:", (lpBalanceAfter - lpBalanceBefore) / 1e18);
console2.log("Simulation successful");
}

function _sortAscending(uint256[] memory arr) internal pure {
uint256 length = arr.length;
for (uint256 i = 1; i < length; i++) {
uint256 key = arr[i];
uint256 j = i;
while (j > 0 && arr[j - 1] > key) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = key;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import "forge-std/Script.sol";
import "forge-std/console2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Deployed} from "../../deploys/Deployed.s.sol";
import {EtherFiRestaker} from "../../../src/EtherFiRestaker.sol";
import {IDelegationManager} from "../../../src/eigenlayer-interfaces/IDelegationManager.sol";

// Complete all pending EigenLayer stETH withdrawal queues on the restaker:
// forge script script/operations/steth-management/CompleteQueuedWithdrawalsStETH.s.sol --fork-url $MAINNET_RPC_URL -vvvv

contract CompleteQueuedWithdrawalsStETH is Script, Deployed {

EtherFiRestaker constant etherFiRestaker = EtherFiRestaker(payable(ETHERFI_RESTAKER));

function run() external {
IDelegationManager delegationManager = etherFiRestaker.eigenLayerDelegationManager();
address lido = address(etherFiRestaker.lido());
uint32 delayBlocks = delegationManager.minWithdrawalDelayBlocks();

console2.log("=== EtherFi Restaker: Complete Queued stETH Withdrawals ===");
console2.log("Restaker:", address(etherFiRestaker));
console2.log("Current block:", block.number);
console2.log("Min withdrawal delay blocks:", delayBlocks);
console2.log("");

// Fetch all pending withdrawal roots tracked by the restaker
bytes32[] memory allRoots = etherFiRestaker.pendingWithdrawalRoots();
console2.log("Total pending withdrawal roots:", allRoots.length);
require(allRoots.length > 0, "No pending withdrawal roots");

// Filter for completable roots (past the delay)
uint256 completableCount = 0;
bool[] memory isCompletable = new bool[](allRoots.length);

for (uint256 i = 0; i < allRoots.length; i++) {
(IDelegationManager.Withdrawal memory w,) = delegationManager.getQueuedWithdrawal(allRoots[i]);

bool pastDelay = block.number >= uint256(w.startBlock) + uint256(delayBlocks);
isCompletable[i] = pastDelay;

console2.log("---");
console2.log(" root:");
console2.logBytes32(allRoots[i]);
console2.log(" startBlock:", w.startBlock);
console2.log(" completableAtBlock:", uint256(w.startBlock) + uint256(delayBlocks));
console2.log(" ready:", pastDelay ? "YES" : "NO");

if (pastDelay) completableCount++;
}

console2.log("");
console2.log("Completable withdrawals:", completableCount, "of", allRoots.length);
require(completableCount > 0, "No withdrawals ready to complete yet");

// Build arrays for only the completable withdrawals
IDelegationManager.Withdrawal[] memory withdrawals = new IDelegationManager.Withdrawal[](completableCount);
IERC20[][] memory tokens = new IERC20[][](completableCount);

uint256 idx = 0;
for (uint256 i = 0; i < allRoots.length; i++) {
if (!isCompletable[i]) continue;

(IDelegationManager.Withdrawal memory w,) = delegationManager.getQueuedWithdrawal(allRoots[i]);
withdrawals[idx] = w;

tokens[idx] = new IERC20[](w.strategies.length);
for (uint256 j = 0; j < w.strategies.length; j++) {
tokens[idx][j] = w.strategies[j].underlyingToken();
}
idx++;
}

// Log calldata for Gnosis Safe
bytes memory callData = abi.encodeWithSelector(
EtherFiRestaker.completeQueuedWithdrawals.selector,
withdrawals,
tokens
);
console2.log("");
console2.log("=== completeQueuedWithdrawals calldata ===");
console2.log("Target:", address(etherFiRestaker));
console2.logBytes(callData);

// Simulate the completion on fork
console2.log("");
console2.log("=== Simulating completion on fork ===");
uint256 stEthBefore = IERC20(lido).balanceOf(address(etherFiRestaker));

vm.prank(ETHERFI_OPERATING_ADMIN);
etherFiRestaker.completeQueuedWithdrawals(withdrawals, tokens);

uint256 stEthAfter = IERC20(lido).balanceOf(address(etherFiRestaker));
console2.log("stETH received by restaker:", stEthAfter - stEthBefore);
console2.log("Remaining pending roots:", etherFiRestaker.pendingWithdrawalRoots().length);
console2.log("Remaining restaked stETH:", etherFiRestaker.getRestakedAmount(lido));
console2.log("Simulation successful");

// bytes memory callData2 = abi.encodeWithSignature(
// "stEthRequestWithdrawal(uint256)",
// 50000 ether
// );
// console2.log("");
// console2.log("=== stEthRequestWithdrawal calldata ===");
// console2.log("Target:", address(etherFiRestaker));
// console2.logBytes(callData2);

// vm.prank(ETHERFI_OPERATING_ADMIN);
// etherFiRestaker.stEthRequestWithdrawal(50000 ether);
Comment thread
pankajjagtapp marked this conversation as resolved.
}
}
4 changes: 2 additions & 2 deletions script/operations/steth-management/UnrestakeStEth.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {EtherFiRestaker} from "../../../src/EtherFiRestaker.sol";
import {IDelegationManager} from "../../../src/eigenlayer-interfaces/IDelegationManager.sol";

// Full un-restake (all stETH restaked in EigenLayer):
// FULL_WITHDRAWAL=true forge script script/operations/steth-claim-withdrawals/UnrestakeStEth.s.sol --fork-url $MAINNET_RPC_URL -vvvv
// FULL_WITHDRAWAL=true forge script script/operations/steth-management/UnrestakeStEth.s.sol --fork-url $MAINNET_RPC_URL -vvvv
//
// Partial un-restake (amount in ether):
// AMOUNT=100 forge script script/operations/steth-claim-withdrawals/UnrestakeStEth.s.sol --fork-url $MAINNET_RPC_URL -vvvv
// AMOUNT=100 forge script script/operations/steth-management/UnrestakeStEth.s.sol --fork-url $MAINNET_RPC_URL -vvvv

contract UnrestakeStEth is Script, Deployed {

Expand Down
12 changes: 7 additions & 5 deletions test/integration-tests/Handle-Remainder-Shares.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,18 @@ contract HandleRemainderSharesIntegrationTest is TestSetup, Deployed {
roleRegistryInstance.grantRole(withdrawRequestNFTInstance.IMPLICIT_FEE_CLAIMER_ROLE(), alice);
vm.stopPrank();

uint256 treasuryBalanceBefore = eETHInstance.balanceOf(buybackWallet);
uint256 nominalToTreasury = Math.mulDiv(remainderAmount, splitRatios[i], 10000);
uint256 expectedSharesToTreasury = liquidityPoolInstance.sharesForAmount(nominalToTreasury);

uint256 treasurySharesBefore = eETHInstance.shares(buybackWallet);

vm.prank(alice);
withdrawRequestNFTInstance.handleRemainder(remainderAmount);

uint256 treasuryBalanceAfter = eETHInstance.balanceOf(buybackWallet);
uint256 expectedToTreasury = Math.mulDiv(remainderAmount, splitRatios[i], 10000);
uint256 treasurySharesAfter = eETHInstance.shares(buybackWallet);

assertApproxEqAbs(treasuryBalanceAfter - treasuryBalanceBefore, expectedToTreasury, 1e14,
string(abi.encodePacked("Treasury should receive correct portion for ratio ", vm.toString(splitRatios[i]))));
assertApproxEqAbs(treasurySharesAfter - treasurySharesBefore, expectedSharesToTreasury, 10,
string(abi.encodePacked("Treasury should receive correct shares for ratio ", vm.toString(splitRatios[i]))));
}
}
}
14 changes: 14 additions & 0 deletions test/integration-tests/Validator-Flows.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ contract ValidatorFlowsIntegrationTest is TestSetup, Deployed {

/// @dev Advances the admin's lastHandledReportRefSlot to match the oracle's lastPublishedReportRefSlot.
function _syncOracleReportState() internal {
// Step A: sync admin to oracle so submitReport doesn't fail with
// "Last published report is not handled yet".
uint32 lastPublished = etherFiOracleInstance.lastPublishedReportRefSlot();
uint32 lastHandled = etherFiAdminInstance.lastHandledReportRefSlot();

Expand All @@ -34,6 +36,18 @@ contract ValidatorFlowsIntegrationTest is TestSetup, Deployed {
val |= uint256(lastPublishedBlock) << 32;
vm.store(address(etherFiAdminInstance), bytes32(uint256(209)), bytes32(val));
}

// Step B: unconditionally reset operator submissions by removing and re-adding each one.
// addCommitteeMember() resets CommitteeMemberState to
// (registered=true, enabled=true, lastReportRefSlot=0, numReports=0), clearing any stale
// submission from mainnet without adding new committee members.
address oracleOwner = etherFiOracleInstance.owner();
vm.startPrank(oracleOwner);
etherFiOracleInstance.removeCommitteeMember(AVS_OPERATOR_1);
etherFiOracleInstance.addCommitteeMember(AVS_OPERATOR_1);
etherFiOracleInstance.removeCommitteeMember(AVS_OPERATOR_2);
etherFiOracleInstance.addCommitteeMember(AVS_OPERATOR_2);
vm.stopPrank();
}

function _toArray(IStakingManager.DepositData memory d) internal pure returns (IStakingManager.DepositData[] memory arr) {
Expand Down
38 changes: 28 additions & 10 deletions test/integration-tests/Withdraw.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,44 @@ contract WithdrawIntegrationTest is TestSetup, Deployed {
_syncOracleReportState();
}

/// @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.
/// @dev Syncs oracle/admin state so AVS_OPERATOR_1 and AVS_OPERATOR_2 can submit reports.
///
/// Two conditions can block report submission on a mainnet fork:
///
/// (A) Admin hasn't processed the latest published report yet:
/// shouldSubmitReport() requires lastPublishedReportRefSlot == lastHandledReportRefSlot.
/// Fix: advance admin's lastHandledReportRefSlot to match the oracle.
///
/// (B) Operators already submitted for slotForNextReport() (quorum not reached on mainnet):
/// shouldSubmitReport() returns false when lastReportRefSlot >= slotForNextReport().
/// Fix: remove and re-add each operator via the oracle owner. addCommitteeMember()
/// resets CommitteeMemberState to (registered=true, enabled=true, lastReportRefSlot=0),
/// clearing the stale submission without adding new committee members.
function _syncOracleReportState() internal {
// Step A: sync admin to oracle
uint32 lastPublished = etherFiOracleInstance.lastPublishedReportRefSlot();
uint32 lastHandled = etherFiAdminInstance.lastHandledReportRefSlot();

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(0xFFFFFFFFFFFFFFFF); // clear bits 0-63
val |= uint256(lastPublished);
val |= uint256(lastPublishedBlock) << 32;
vm.store(address(etherFiAdminInstance), bytes32(uint256(209)), bytes32(val));
}

// Step B: unconditionally reset operator submissions by removing and re-adding each one.
// addCommitteeMember() resets CommitteeMemberState to
// (registered=true, enabled=true, lastReportRefSlot=0, numReports=0), clearing any stale
// submission from mainnet without adding new committee members.
address oracleOwner = etherFiOracleInstance.owner();
vm.startPrank(oracleOwner);
etherFiOracleInstance.removeCommitteeMember(AVS_OPERATOR_1);
etherFiOracleInstance.addCommitteeMember(AVS_OPERATOR_1);
etherFiOracleInstance.removeCommitteeMember(AVS_OPERATOR_2);
etherFiOracleInstance.addCommitteeMember(AVS_OPERATOR_2);
vm.stopPrank();
}

function test_Withdraw_EtherFiRedemptionManager_redeemEEth() public {
Expand Down
Loading