From 3ac8cddd1dfcb801191a2efa62efa22bd93e6e4a Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 22 Jan 2026 15:06:45 -0500 Subject: [PATCH 01/19] init --- src/LiquidityPool.sol | 46 +- src/PriorityWithdrawalQueue.sol | 723 ++++++++++++++++++++ src/interfaces/ILiquidityPool.sol | 5 + src/interfaces/IPriorityWithdrawalQueue.sol | 77 +++ test/PriorityWithdrawalQueue.t.sol | 645 +++++++++++++++++ 5 files changed, 1494 insertions(+), 2 deletions(-) create mode 100644 src/PriorityWithdrawalQueue.sol create mode 100644 src/interfaces/IPriorityWithdrawalQueue.sol create mode 100644 test/PriorityWithdrawalQueue.t.sol diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index b35361aea..c345bc26b 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -68,6 +68,10 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL IRoleRegistry public roleRegistry; uint256 public validatorSizeWei; + + address public priorityWithdrawalQueue; + uint128 public ethAmountLockedForPriorityWithdrawal; + //-------------------------------------------------------------------------------------- //------------------------------------- ROLES --------------------------------------- //-------------------------------------------------------------------------------------- @@ -203,13 +207,30 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL /// it returns the amount of shares burned function withdraw(address _recipient, uint256 _amount) external whenNotPaused returns (uint256) { uint256 share = sharesForWithdrawalAmount(_amount); - require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiRedemptionManager), "Incorrect Caller"); - if (totalValueInLp < _amount || (msg.sender == address(withdrawRequestNFT) && ethAmountLockedForWithdrawal < _amount) || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); + require( + msg.sender == address(withdrawRequestNFT) || + msg.sender == address(membershipManager) || + msg.sender == address(etherFiRedemptionManager) || + msg.sender == priorityWithdrawalQueue, + "Incorrect Caller" + ); + + // Check liquidity based on caller + if (msg.sender == address(withdrawRequestNFT)) { + if (totalValueInLp < _amount || ethAmountLockedForWithdrawal < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); + } else if (msg.sender == priorityWithdrawalQueue) { + if (totalValueInLp < _amount || ethAmountLockedForPriorityWithdrawal < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); + } else { + if (totalValueInLp < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); + } + if (_amount > type(uint128).max || _amount == 0 || share == 0) revert InvalidAmount(); totalValueInLp -= uint128(_amount); if (msg.sender == address(withdrawRequestNFT)) { ethAmountLockedForWithdrawal -= uint128(_amount); + } else if (msg.sender == priorityWithdrawalQueue) { + ethAmountLockedForPriorityWithdrawal -= uint128(_amount); } eETH.burnShares(msg.sender, share); @@ -474,6 +495,27 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL ethAmountLockedForWithdrawal += _amount; } + /// @notice Set the priority withdrawal queue address + /// @param _priorityWithdrawalQueue Address of the PriorityWithdrawalQueue contract + function setPriorityWithdrawalQueue(address _priorityWithdrawalQueue) external { + if (!roleRegistry.hasRole(LIQUIDITY_POOL_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + priorityWithdrawalQueue = _priorityWithdrawalQueue; + } + + /// @notice Add ETH amount locked for priority withdrawal + /// @param _amount Amount of ETH to lock + function addEthAmountLockedForPriorityWithdrawal(uint128 _amount) external { + if (msg.sender != priorityWithdrawalQueue) revert IncorrectCaller(); + ethAmountLockedForPriorityWithdrawal += _amount; + } + + /// @notice Reduce ETH amount locked for priority withdrawal (admin function for emergency) + /// @param _amount Amount of ETH to unlock + function reduceEthAmountLockedForPriorityWithdrawal(uint128 _amount) external { + if (!roleRegistry.hasRole(LIQUIDITY_POOL_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + ethAmountLockedForPriorityWithdrawal -= _amount; + } + function burnEEthShares(uint256 shares) external { if (msg.sender != address(etherFiRedemptionManager) && msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); eETH.burnShares(msg.sender, shares); diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol new file mode 100644 index 000000000..c7ca4e598 --- /dev/null +++ b/src/PriorityWithdrawalQueue.sol @@ -0,0 +1,723 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./interfaces/IPriorityWithdrawalQueue.sol"; +import "./interfaces/ILiquidityPool.sol"; +import "./interfaces/IeETH.sol"; +import "./interfaces/IRoleRegistry.sol"; + +/// @title PriorityWithdrawalQueue +/// @notice Manages priority withdrawals for whitelisted VIP users using hash-based request tracking +/// @dev Implements BoringOnChainQueue patterns with WithdrawRequestNFT validation checks +contract PriorityWithdrawalQueue is + Initializable, + OwnableUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + IPriorityWithdrawalQueue +{ + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.Bytes32Set; + using Math for uint256; + + //-------------------------------------------------------------------------------------- + //--------------------------------- CONSTANTS ---------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Maximum time in seconds a withdraw request can take to mature + uint24 public constant MAXIMUM_SECONDS_TO_MATURITY = 30 days; + + /// @notice Maximum minimum validity period after maturity + uint24 public constant MAXIMUM_MINIMUM_SECONDS_TO_DEADLINE = 30 days; + + //-------------------------------------------------------------------------------------- + //--------------------------------- STATE-VARIABLES ---------------------------------- + //-------------------------------------------------------------------------------------- + + ILiquidityPool public liquidityPool; + IeETH public eETH; + IRoleRegistry public roleRegistry; + + /// @notice EnumerableSet to store all active withdraw request IDs + EnumerableSet.Bytes32Set private _withdrawRequests; + + /// @notice Set of finalized request IDs (fulfilled and ready for claim) + EnumerableSet.Bytes32Set private _finalizedRequests; + + /// @notice Set of invalidated request IDs + mapping(bytes32 => bool) public invalidatedRequests; + + /// @notice Mapping of whitelisted addresses + mapping(address => bool) public isWhitelisted; + + /// @notice Withdrawal configuration + WithdrawConfig private _withdrawConfig; + + /// @notice Request nonce to prevent hash collisions + uint96 public nonce; + + /// @notice Total eETH shares held for pending requests + uint256 public totalPendingShares; + + /// @notice Total eETH shares held for finalized (claimable) requests + uint256 public totalFinalizedShares; + + /// @notice Remainder shares from claimed withdrawals (difference between request shares and actual burned) + uint256 public totalRemainderShares; + + /// @notice Contract pause state + bool public paused; + + //-------------------------------------------------------------------------------------- + //------------------------------------- ROLES ---------------------------------------- + //-------------------------------------------------------------------------------------- + + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE"); + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE"); + + //-------------------------------------------------------------------------------------- + //------------------------------------- EVENTS --------------------------------------- + //-------------------------------------------------------------------------------------- + + event Paused(address account); + event Unpaused(address account); + event WithdrawRequestCreated( + bytes32 indexed requestId, + address indexed user, + uint96 nonce, + uint128 amountOfEEth, + uint128 shareOfEEth, + uint40 creationTime, + uint24 secondsToMaturity, + uint24 secondsToDeadline + ); + event WithdrawRequestCancelled(bytes32 indexed requestId, address indexed user, uint256 timestamp); + event WithdrawRequestFinalized(bytes32 indexed requestId, address indexed user, uint256 timestamp); + event WithdrawRequestClaimed(bytes32 indexed requestId, address indexed user, uint256 amountClaimed, uint256 sharesBurned); + event WithdrawRequestInvalidated(bytes32 indexed requestId); + event WithdrawRequestValidated(bytes32 indexed requestId); + event WhitelistUpdated(address indexed user, bool status); + event WithdrawConfigUpdated(uint24 secondsToMaturity, uint24 minimumSecondsToDeadline, uint96 minimumAmount); + event WithdrawCapacityUpdated(uint256 withdrawCapacity); + event WithdrawsStopped(); + event RemainderHandled(uint256 remainderAmount, uint256 remainderShares); + + //-------------------------------------------------------------------------------------- + //------------------------------------- ERRORS --------------------------------------- + //-------------------------------------------------------------------------------------- + + error NotWhitelisted(); + error InvalidAmount(); + error InvalidDeadline(); + error RequestNotFound(); + error RequestNotFinalized(); + error RequestInvalidated(); + error RequestAlreadyFinalized(); + error NotRequestOwner(); + error IncorrectRole(); + error ContractPaused(); + error ContractNotPaused(); + error WithdrawsNotAllowed(); + error NotEnoughWithdrawCapacity(); + error NotMatured(); + error DeadlinePassed(); + error Keccak256Collision(); + error InvalidConfig(); + error PermitFailedAndAllowanceTooLow(); + error BadInput(); + + //-------------------------------------------------------------------------------------- + //----------------------------------- MODIFIERS -------------------------------------- + //-------------------------------------------------------------------------------------- + + modifier whenNotPaused() { + if (paused) revert ContractPaused(); + _; + } + + modifier onlyWhitelisted() { + if (!isWhitelisted[msg.sender]) revert NotWhitelisted(); + _; + } + + modifier onlyAdmin() { + if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + _; + } + + modifier onlyOracle() { + if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE, msg.sender)) revert IncorrectRole(); + _; + } + + modifier onlyRequestUser(address requestUser) { + if (requestUser != msg.sender) revert NotRequestOwner(); + _; + } + + //-------------------------------------------------------------------------------------- + //---------------------------- STATE-CHANGING FUNCTIONS ------------------------------ + //-------------------------------------------------------------------------------------- + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the contract + /// @param _liquidityPool Address of the LiquidityPool contract + /// @param _eETH Address of the eETH contract + /// @param _roleRegistry Address of the RoleRegistry contract + function initialize( + address _liquidityPool, + address _eETH, + address _roleRegistry + ) external initializer { + if (_liquidityPool == address(0) || _eETH == address(0) || _roleRegistry == address(0)) { + revert BadInput(); + } + + __Ownable_init(); + __UUPSUpgradeable_init(); + __ReentrancyGuard_init(); + + liquidityPool = ILiquidityPool(_liquidityPool); + eETH = IeETH(_eETH); + roleRegistry = IRoleRegistry(_roleRegistry); + + nonce = 1; + paused = false; + + // Default config - can be updated by admin + _withdrawConfig = WithdrawConfig({ + allowWithdraws: true, + secondsToMaturity: 0, // Instant maturity by default for priority users + minimumSecondsToDeadline: 1 days, + minimumAmount: 0.01 ether, + withdrawCapacity: type(uint256).max + }); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ USER FUNCTIONS -------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Request a withdrawal of eETH + /// @param amountOfEEth Amount of eETH to withdraw + /// @param secondsToDeadline Time in seconds the request is valid for after maturity + /// @return requestId The hash-based ID of the created withdrawal request + function requestWithdraw( + uint128 amountOfEEth, + uint24 secondsToDeadline + ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { + _decrementWithdrawCapacity(amountOfEEth); + _validateNewRequest(amountOfEEth, secondsToDeadline); + + IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); + + (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth, secondsToDeadline); + } + + /// @notice Request a withdrawal with permit for gasless approval + /// @param amountOfEEth Amount of eETH to withdraw + /// @param secondsToDeadline Time in seconds the request is valid for after maturity + /// @param permit Permit signature data for eETH approval + /// @return requestId The hash-based ID of the created withdrawal request + function requestWithdrawWithPermit( + uint128 amountOfEEth, + uint24 secondsToDeadline, + PermitInput calldata permit + ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { + _decrementWithdrawCapacity(amountOfEEth); + _validateNewRequest(amountOfEEth, secondsToDeadline); + + // Try permit - continue if it fails (may already be approved) + try eETH.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} + catch { + if (IERC20(address(eETH)).allowance(msg.sender, address(this)) < amountOfEEth) { + revert PermitFailedAndAllowanceTooLow(); + } + } + + IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); + + (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth, secondsToDeadline); + } + + /// @notice Cancel a pending withdrawal request + /// @param request The withdrawal request to cancel + /// @return requestId The cancelled request ID + function cancelWithdraw( + WithdrawRequest calldata request + ) external whenNotPaused onlyRequestUser(request.user) returns (bytes32 requestId) { + requestId = _cancelWithdrawRequest(request); + } + + /// @notice Replace an existing withdrawal request with new parameters + /// @param oldRequest The existing request to replace + /// @param newSecondsToDeadline New validity period for the replacement request + /// @return oldRequestId The cancelled request ID + /// @return newRequestId The new request ID + function replaceWithdraw( + WithdrawRequest calldata oldRequest, + uint24 newSecondsToDeadline + ) external whenNotPaused onlyRequestUser(oldRequest.user) returns (bytes32 oldRequestId, bytes32 newRequestId) { + _validateNewRequest(oldRequest.amountOfEEth, newSecondsToDeadline); + + // Dequeue old request (no capacity increment since we're replacing) + oldRequestId = _dequeueWithdrawRequest(oldRequest); + + emit WithdrawRequestCancelled(oldRequestId, oldRequest.user, block.timestamp); + + // Queue new request with same amount (no capacity decrement) + (newRequestId,) = _queueWithdrawRequest(oldRequest.user, oldRequest.amountOfEEth, newSecondsToDeadline); + } + + /// @notice Claim ETH for a finalized withdrawal request + /// @param request The withdrawal request to claim + function claimWithdraw(WithdrawRequest calldata request) external whenNotPaused nonReentrant { + _claimWithdraw(request, request.user); + } + + /// @notice Batch claim multiple withdrawal requests + /// @param requests Array of withdrawal requests to claim + function batchClaimWithdraw(WithdrawRequest[] calldata requests) external whenNotPaused nonReentrant { + for (uint256 i = 0; i < requests.length; ++i) { + _claimWithdraw(requests[i], requests[i].user); + } + } + + //-------------------------------------------------------------------------------------- + //---------------------------- ORACLE/SOLVER FUNCTIONS ------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Oracle finalizes withdrawal requests after maturity + /// @dev Checks maturity and deadline, marks requests as finalized + /// @param requests Array of requests to finalize + function fulfillRequests(WithdrawRequest[] calldata requests) external onlyOracle whenNotPaused { + uint256 totalSharesToFinalize = 0; + + for (uint256 i = 0; i < requests.length; ++i) { + WithdrawRequest calldata request = requests[i]; + bytes32 requestId = keccak256(abi.encode(request)); + + // Verify request exists in pending set + if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + + // Check not already finalized + if (_finalizedRequests.contains(requestId)) revert RequestAlreadyFinalized(); + + // Check not invalidated + if (invalidatedRequests[requestId]) revert RequestInvalidated(); + + // Check maturity + uint256 maturity = request.creationTime + request.secondsToMaturity; + if (block.timestamp < maturity) revert NotMatured(); + + // Check deadline + uint256 deadline = maturity + request.secondsToDeadline; + if (block.timestamp > deadline) revert DeadlinePassed(); + + // Add to finalized set + _finalizedRequests.add(requestId); + totalSharesToFinalize += request.shareOfEEth; + + emit WithdrawRequestFinalized(requestId, request.user, block.timestamp); + } + + // Update accounting + totalPendingShares -= totalSharesToFinalize; + totalFinalizedShares += totalSharesToFinalize; + + // Lock ETH in LiquidityPool for priority withdrawals + uint256 totalAmountToLock = liquidityPool.amountForShare(totalSharesToFinalize); + liquidityPool.addEthAmountLockedForPriorityWithdrawal(uint128(totalAmountToLock)); + } + + //-------------------------------------------------------------------------------------- + //----------------------------------- ADMIN FUNCTIONS -------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Add an address to the whitelist + /// @param user Address to whitelist + function addToWhitelist(address user) external onlyAdmin { + if (user == address(0)) revert BadInput(); + isWhitelisted[user] = true; + emit WhitelistUpdated(user, true); + } + + /// @notice Remove an address from the whitelist + /// @param user Address to remove from whitelist + function removeFromWhitelist(address user) external onlyAdmin { + isWhitelisted[user] = false; + emit WhitelistUpdated(user, false); + } + + /// @notice Batch update whitelist status + /// @param users Array of user addresses + /// @param statuses Array of whitelist statuses + function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external onlyAdmin { + if (users.length != statuses.length) revert BadInput(); + for (uint256 i = 0; i < users.length; ++i) { + if (users[i] == address(0)) revert BadInput(); + isWhitelisted[users[i]] = statuses[i]; + emit WhitelistUpdated(users[i], statuses[i]); + } + } + + /// @notice Update withdrawal configuration + /// @param secondsToMaturity Time until requests can be fulfilled + /// @param minimumSecondsToDeadline Minimum validity period after maturity + /// @param minimumAmount Minimum withdrawal amount + function updateWithdrawConfig( + uint24 secondsToMaturity, + uint24 minimumSecondsToDeadline, + uint96 minimumAmount + ) external onlyAdmin { + if (secondsToMaturity > MAXIMUM_SECONDS_TO_MATURITY) revert InvalidConfig(); + if (minimumSecondsToDeadline > MAXIMUM_MINIMUM_SECONDS_TO_DEADLINE) revert InvalidConfig(); + + _withdrawConfig.secondsToMaturity = secondsToMaturity; + _withdrawConfig.minimumSecondsToDeadline = minimumSecondsToDeadline; + _withdrawConfig.minimumAmount = minimumAmount; + _withdrawConfig.allowWithdraws = true; + + emit WithdrawConfigUpdated(secondsToMaturity, minimumSecondsToDeadline, minimumAmount); + } + + /// @notice Set the withdrawal capacity + /// @param capacity New withdrawal capacity + function setWithdrawCapacity(uint256 capacity) external onlyAdmin { + _withdrawConfig.withdrawCapacity = capacity; + emit WithdrawCapacityUpdated(capacity); + } + + /// @notice Stop all withdrawals + function stopWithdraws() external onlyAdmin { + _withdrawConfig.allowWithdraws = false; + emit WithdrawsStopped(); + } + + /// @notice Invalidate a withdrawal request (prevents finalization) + /// @param request The request to invalidate + function invalidateRequest(WithdrawRequest calldata request) external onlyAdmin { + bytes32 requestId = keccak256(abi.encode(request)); + if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + if (invalidatedRequests[requestId]) revert RequestInvalidated(); + + invalidatedRequests[requestId] = true; + emit WithdrawRequestInvalidated(requestId); + } + + /// @notice Validate a previously invalidated request + /// @param requestId The request ID to validate + function validateRequest(bytes32 requestId) external onlyAdmin { + if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + if (!invalidatedRequests[requestId]) revert BadInput(); + + invalidatedRequests[requestId] = false; + emit WithdrawRequestValidated(requestId); + } + + /// @notice Bulk finalize requests up to a certain request ID + /// @dev Used for batch finalization by admin + /// @param upToRequestId The request ID to finalize up to + function finalizeRequests(bytes32 upToRequestId) external onlyAdmin { + // This function allows admin to mark a specific request as finalized + // Useful for edge cases where oracle flow is bypassed + if (!_withdrawRequests.contains(upToRequestId)) revert RequestNotFound(); + if (_finalizedRequests.contains(upToRequestId)) revert RequestAlreadyFinalized(); + + _finalizedRequests.add(upToRequestId); + } + + /// @notice Admin cancel multiple user withdrawals + /// @param requests Array of requests to cancel + /// @return cancelledRequestIds Array of cancelled request IDs + function cancelUserWithdraws( + WithdrawRequest[] calldata requests + ) external onlyAdmin returns (bytes32[] memory cancelledRequestIds) { + uint256 length = requests.length; + cancelledRequestIds = new bytes32[](length); + + for (uint256 i = 0; i < length; ++i) { + cancelledRequestIds[i] = _cancelWithdrawRequest(requests[i]); + } + } + + /// @notice Handle remainder shares (from rounding differences) + /// @param sharesToBurn Amount of remainder shares to burn + function handleRemainder(uint256 sharesToBurn) external onlyAdmin { + if (sharesToBurn > totalRemainderShares) revert BadInput(); + + uint256 amountToBurn = liquidityPool.amountForShare(sharesToBurn); + totalRemainderShares -= sharesToBurn; + + // Burn the remainder + liquidityPool.burnEEthShares(sharesToBurn); + + emit RemainderHandled(amountToBurn, sharesToBurn); + } + + /// @notice Pause the contract + function pauseContract() external { + if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_PAUSER(), msg.sender)) revert IncorrectRole(); + if (paused) revert ContractPaused(); + paused = true; + emit Paused(msg.sender); + } + + /// @notice Unpause the contract + function unPauseContract() external { + if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_UNPAUSER(), msg.sender)) revert IncorrectRole(); + if (!paused) revert ContractNotPaused(); + paused = false; + emit Unpaused(msg.sender); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ INTERNAL FUNCTIONS ---------------------------------- + //-------------------------------------------------------------------------------------- + + /// @dev Validate new request parameters + function _validateNewRequest(uint128 amountOfEEth, uint24 secondsToDeadline) internal view { + if (!_withdrawConfig.allowWithdraws) revert WithdrawsNotAllowed(); + if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); + if (secondsToDeadline < _withdrawConfig.minimumSecondsToDeadline) revert InvalidDeadline(); + } + + /// @dev Decrement withdrawal capacity + function _decrementWithdrawCapacity(uint128 amount) internal { + if (_withdrawConfig.withdrawCapacity < type(uint256).max) { + if (_withdrawConfig.withdrawCapacity < amount) revert NotEnoughWithdrawCapacity(); + _withdrawConfig.withdrawCapacity -= amount; + emit WithdrawCapacityUpdated(_withdrawConfig.withdrawCapacity); + } + } + + /// @dev Increment withdrawal capacity + function _incrementWithdrawCapacity(uint128 amount) internal { + if (_withdrawConfig.withdrawCapacity < type(uint256).max) { + _withdrawConfig.withdrawCapacity += amount; + emit WithdrawCapacityUpdated(_withdrawConfig.withdrawCapacity); + } + } + + /// @dev Queue a new withdrawal request + function _queueWithdrawRequest( + address user, + uint128 amountOfEEth, + uint24 secondsToDeadline + ) internal returns (bytes32 requestId, WithdrawRequest memory req) { + uint96 requestNonce; + unchecked { + requestNonce = nonce++; + } + + uint128 shareOfEEth = uint128(liquidityPool.sharesForAmount(amountOfEEth)); + if (shareOfEEth == 0) revert InvalidAmount(); + + uint40 timeNow = uint40(block.timestamp); + + req = WithdrawRequest({ + nonce: requestNonce, + user: user, + amountOfEEth: amountOfEEth, + shareOfEEth: shareOfEEth, + creationTime: timeNow, + secondsToMaturity: _withdrawConfig.secondsToMaturity, + secondsToDeadline: secondsToDeadline + }); + + requestId = keccak256(abi.encode(req)); + + bool addedToSet = _withdrawRequests.add(requestId); + if (!addedToSet) revert Keccak256Collision(); + + totalPendingShares += shareOfEEth; + + emit WithdrawRequestCreated( + requestId, + user, + requestNonce, + amountOfEEth, + shareOfEEth, + timeNow, + _withdrawConfig.secondsToMaturity, + secondsToDeadline + ); + } + + /// @dev Dequeue a withdrawal request + function _dequeueWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { + requestId = keccak256(abi.encode(request)); + bool removedFromSet = _withdrawRequests.remove(requestId); + if (!removedFromSet) revert RequestNotFound(); + + // Also remove from finalized if it was there + _finalizedRequests.remove(requestId); + } + + /// @dev Cancel a withdrawal request and return eETH to user + function _cancelWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { + requestId = _dequeueWithdrawRequest(request); + + // Update accounting based on whether it was finalized + if (_finalizedRequests.contains(requestId)) { + totalFinalizedShares -= request.shareOfEEth; + // Unlock ETH from LiquidityPool + uint256 amountToUnlock = liquidityPool.amountForShare(request.shareOfEEth); + liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToUnlock)); + } else { + totalPendingShares -= request.shareOfEEth; + } + + _incrementWithdrawCapacity(request.amountOfEEth); + + // Return eETH to user + IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); + + emit WithdrawRequestCancelled(requestId, request.user, block.timestamp); + } + + /// @dev Internal claim function + function _claimWithdraw(WithdrawRequest calldata request, address recipient) internal { + if (request.user != msg.sender) revert NotRequestOwner(); + + bytes32 requestId = keccak256(abi.encode(request)); + + // Verify request exists + if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + + // Verify request is finalized + if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); + + // Verify not invalidated + if (invalidatedRequests[requestId]) revert RequestInvalidated(); + + // Calculate claimable amount (min of requested amount or current share value) + // This protects users if rate has changed unfavorably, and protocol if rate increased + uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); + uint256 amountToWithdraw = request.amountOfEEth < amountForShares + ? request.amountOfEEth + : amountForShares; + + uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); + + // Remove from sets + _withdrawRequests.remove(requestId); + _finalizedRequests.remove(requestId); + + // Track remainder (difference between original shares and burned shares) + uint256 remainder = request.shareOfEEth > sharesToBurn + ? request.shareOfEEth - sharesToBurn + : 0; + totalRemainderShares += remainder; + + // Update accounting + totalFinalizedShares -= request.shareOfEEth; + + // Execute withdrawal through LiquidityPool + uint256 burnedShares = liquidityPool.withdraw(recipient, amountToWithdraw); + assert(burnedShares == sharesToBurn); + + emit WithdrawRequestClaimed(requestId, recipient, amountToWithdraw, burnedShares); + } + + function _authorizeUpgrade(address) internal override { + roleRegistry.onlyProtocolUpgrader(msg.sender); + } + + //-------------------------------------------------------------------------------------- + //------------------------------------ GETTERS --------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Get the request ID from a request struct + /// @param request The withdrawal request + /// @return requestId The keccak256 hash of the request + function getRequestId(WithdrawRequest calldata request) external pure returns (bytes32) { + return keccak256(abi.encode(request)); + } + + /// @notice Get all active request IDs + /// @return Array of request IDs + function getRequestIds() external view returns (bytes32[] memory) { + return _withdrawRequests.values(); + } + + /// @notice Get all finalized request IDs + /// @return Array of finalized request IDs + function getFinalizedRequestIds() external view returns (bytes32[] memory) { + return _finalizedRequests.values(); + } + + /// @notice Check if a request exists + /// @param requestId The request ID to check + /// @return Whether the request exists + function requestExists(bytes32 requestId) external view returns (bool) { + return _withdrawRequests.contains(requestId); + } + + /// @notice Check if a request is finalized + /// @param requestId The request ID to check + /// @return Whether the request is finalized + function isFinalized(bytes32 requestId) external view returns (bool) { + return _finalizedRequests.contains(requestId); + } + + /// @notice Get the claimable amount for a request + /// @param request The withdrawal request + /// @return The claimable ETH amount + function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256) { + bytes32 requestId = keccak256(abi.encode(request)); + if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); + if (invalidatedRequests[requestId]) revert RequestInvalidated(); + + uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); + return request.amountOfEEth < amountForShares ? request.amountOfEEth : amountForShares; + } + + /// @notice Get the withdrawal configuration + /// @return The withdraw config struct + function withdrawConfig() external view returns (WithdrawConfig memory) { + return _withdrawConfig; + } + + /// @notice Get the total number of active requests + /// @return The number of active requests + function totalActiveRequests() external view returns (uint256) { + return _withdrawRequests.length(); + } + + /// @notice Get the total eETH amount pending (not finalized) + /// @return The total pending eETH amount + function totalPendingAmount() external view returns (uint256) { + return liquidityPool.amountForShare(totalPendingShares); + } + + /// @notice Get the total eETH amount finalized (ready for claim) + /// @return The total finalized eETH amount + function totalFinalizedAmount() external view returns (uint256) { + return liquidityPool.amountForShare(totalFinalizedShares); + } + + /// @notice Get the total remainder amount available + /// @return The total remainder eETH amount + function getRemainderAmount() external view returns (uint256) { + return liquidityPool.amountForShare(totalRemainderShares); + } + + /// @notice Get the implementation address + /// @return The implementation address + function getImplementation() external view returns (address) { + return _getImplementation(); + } +} diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 9741a2bf5..da725d47e 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -51,6 +51,8 @@ interface ILiquidityPool { function amountForShare(uint256 _share) external view returns (uint256); function eETH() external view returns (IeETH); function ethAmountLockedForWithdrawal() external view returns (uint128); + function priorityWithdrawalQueue() external view returns (address); + function ethAmountLockedForPriorityWithdrawal() external view returns (uint128); function deposit() external payable returns (uint256); function deposit(address _referral) external payable returns (uint256); @@ -74,6 +76,9 @@ interface ILiquidityPool { function rebase(int128 _accruedRewards) external; function payProtocolFees(uint128 _protocolFees) external; function addEthAmountLockedForWithdrawal(uint128 _amount) external; + function setPriorityWithdrawalQueue(address _priorityWithdrawalQueue) external; + function addEthAmountLockedForPriorityWithdrawal(uint128 _amount) external; + function reduceEthAmountLockedForPriorityWithdrawal(uint128 _amount) external; function pauseContract() external; function burnEEthShares(uint256 shares) external; diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol new file mode 100644 index 000000000..1a3661d3f --- /dev/null +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +interface IPriorityWithdrawalQueue { + /// @notice Withdrawal request struct stored as hash in EnumerableSet + /// @param nonce Unique nonce to prevent hash collisions + /// @param user The user who created the request + /// @param amountOfEEth Original eETH amount requested + /// @param shareOfEEth eETH shares at time of request + /// @param creationTime Timestamp when request was created + /// @param secondsToMaturity Time until request can be fulfilled + /// @param secondsToDeadline Time after maturity until request expires + struct WithdrawRequest { + uint96 nonce; + address user; + uint128 amountOfEEth; + uint128 shareOfEEth; + uint40 creationTime; + uint24 secondsToMaturity; + uint24 secondsToDeadline; + } + + /// @notice Configuration for withdrawal parameters + /// @param allowWithdraws Whether withdrawals are currently allowed + /// @param secondsToMaturity Time in seconds until a request can be fulfilled + /// @param minimumSecondsToDeadline Minimum validity period after maturity + /// @param minimumAmount Minimum eETH amount per withdrawal + /// @param withdrawCapacity Maximum pending withdrawal amount allowed + struct WithdrawConfig { + bool allowWithdraws; + uint24 secondsToMaturity; + uint24 minimumSecondsToDeadline; + uint96 minimumAmount; + uint256 withdrawCapacity; + } + + struct PermitInput { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + // User functions + function requestWithdraw(uint128 amountOfEEth, uint24 secondsToDeadline) external returns (bytes32 requestId); + function requestWithdrawWithPermit(uint128 amountOfEEth, uint24 secondsToDeadline, PermitInput calldata permit) external returns (bytes32 requestId); + function cancelWithdraw(WithdrawRequest calldata request) external returns (bytes32 requestId); + function replaceWithdraw(WithdrawRequest calldata oldRequest, uint24 newSecondsToDeadline) external returns (bytes32 oldRequestId, bytes32 newRequestId); + function claimWithdraw(WithdrawRequest calldata request) external; + function batchClaimWithdraw(WithdrawRequest[] calldata requests) external; + + // View functions + function getRequestId(WithdrawRequest calldata request) external pure returns (bytes32); + function getRequestIds() external view returns (bytes32[] memory); + function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256); + function isWhitelisted(address user) external view returns (bool); + function nonce() external view returns (uint96); + function withdrawConfig() external view returns (WithdrawConfig memory); + + // Oracle/Solver functions + function fulfillRequests(WithdrawRequest[] calldata requests) external; + + // Admin functions + function addToWhitelist(address user) external; + function removeFromWhitelist(address user) external; + function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external; + function updateWithdrawConfig(uint24 secondsToMaturity, uint24 minimumSecondsToDeadline, uint96 minimumAmount) external; + function setWithdrawCapacity(uint256 capacity) external; + function stopWithdraws() external; + function invalidateRequest(WithdrawRequest calldata request) external; + function validateRequest(bytes32 requestId) external; + function finalizeRequests(bytes32 upToRequestId) external; + function cancelUserWithdraws(WithdrawRequest[] calldata requests) external returns (bytes32[] memory); + function pauseContract() external; + function unPauseContract() external; +} diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol new file mode 100644 index 000000000..3c6169a7a --- /dev/null +++ b/test/PriorityWithdrawalQueue.t.sol @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "./TestSetup.sol"; +import "forge-std/console2.sol"; + +import "../src/PriorityWithdrawalQueue.sol"; +import "../src/interfaces/IPriorityWithdrawalQueue.sol"; + +contract PriorityWithdrawalQueueTest is TestSetup { + PriorityWithdrawalQueue public priorityQueue; + PriorityWithdrawalQueue public priorityQueueImplementation; + + address public oracle; + address public vipUser; + address public regularUser; + + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE"); + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE"); + + // Default deadline for tests + uint24 public constant DEFAULT_DEADLINE = 7 days; + + function setUp() public { + setUpTests(); + + // Setup actors + oracle = makeAddr("oracle"); + vipUser = makeAddr("vipUser"); + regularUser = makeAddr("regularUser"); + + // Deploy PriorityWithdrawalQueue + vm.startPrank(owner); + priorityQueueImplementation = new PriorityWithdrawalQueue(); + UUPSProxy proxy = new UUPSProxy( + address(priorityQueueImplementation), + abi.encodeWithSelector( + PriorityWithdrawalQueue.initialize.selector, + address(liquidityPoolInstance), + address(eETHInstance), + address(roleRegistryInstance) + ) + ); + priorityQueue = PriorityWithdrawalQueue(address(proxy)); + + // Grant roles + roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, alice); + roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE, oracle); + vm.stopPrank(); + + // Configure LiquidityPool to use PriorityWithdrawalQueue + vm.prank(alice); + liquidityPoolInstance.setPriorityWithdrawalQueue(address(priorityQueue)); + + // Whitelist the VIP user + vm.prank(alice); + priorityQueue.addToWhitelist(vipUser); + + // Give VIP user some ETH and deposit to get eETH + vm.deal(vipUser, 100 ether); + vm.startPrank(vipUser); + liquidityPoolInstance.deposit{value: 50 ether}(); + vm.stopPrank(); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ HELPER FUNCTIONS ------------------------------------ + //-------------------------------------------------------------------------------------- + + /// @dev Helper to create a withdrawal request and return both the requestId and request struct + function _createWithdrawRequest(address user, uint128 amount, uint24 deadline) + internal + returns (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) + { + uint96 nonceBefore = priorityQueue.nonce(); + uint128 shareAmount = uint128(liquidityPoolInstance.sharesForAmount(amount)); + uint40 timestamp = uint40(block.timestamp); + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + + vm.startPrank(user); + eETHInstance.approve(address(priorityQueue), amount); + requestId = priorityQueue.requestWithdraw(amount, deadline); + vm.stopPrank(); + + // Reconstruct the request struct + request = IPriorityWithdrawalQueue.WithdrawRequest({ + nonce: nonceBefore, + user: user, + amountOfEEth: amount, + shareOfEEth: shareAmount, + creationTime: timestamp, + secondsToMaturity: config.secondsToMaturity, + secondsToDeadline: deadline + }); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ REQUEST TESTS --------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_requestWithdraw() public { + uint128 withdrawAmount = 10 ether; + + // Record initial state + uint256 initialEethBalance = eETHInstance.balanceOf(vipUser); + uint256 initialQueueEethBalance = eETHInstance.balanceOf(address(priorityQueue)); + uint96 initialNonce = priorityQueue.nonce(); + + // Create request + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Verify state changes + assertEq(priorityQueue.nonce(), initialNonce + 1, "Nonce should increment"); + assertEq(eETHInstance.balanceOf(vipUser), initialEethBalance - withdrawAmount, "VIP user eETH balance should decrease"); + assertEq(eETHInstance.balanceOf(address(priorityQueue)), initialQueueEethBalance + withdrawAmount, "Queue eETH balance should increase"); + + // Verify request exists + assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); + assertFalse(priorityQueue.isFinalized(requestId), "Request should not be finalized yet"); + + // Verify request ID matches + bytes32 expectedId = keccak256(abi.encode(request)); + assertEq(requestId, expectedId, "Request ID should match hash of request"); + } + + function test_requestWithdrawWithPermit() public { + uint128 withdrawAmount = 10 ether; + + // For this test, we'll use regular approval since permit requires signatures + // The permit flow is tested by checking the fallback to allowance + + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), withdrawAmount); + + // Create permit input (will fail but fallback to allowance) + IPriorityWithdrawalQueue.PermitInput memory permit = IPriorityWithdrawalQueue.PermitInput({ + value: withdrawAmount, + deadline: block.timestamp + 1 days, + v: 0, + r: bytes32(0), + s: bytes32(0) + }); + + bytes32 requestId = priorityQueue.requestWithdrawWithPermit(withdrawAmount, DEFAULT_DEADLINE, permit); + vm.stopPrank(); + + assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ FULFILL TESTS --------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_fulfillRequests() public { + uint128 withdrawAmount = 10 ether; + + // Setup: VIP user creates a withdrawal request + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Record state before fulfillment + uint256 pendingSharesBefore = priorityQueue.totalPendingShares(); + uint256 finalizedSharesBefore = priorityQueue.totalFinalizedShares(); + uint128 lpLockedBefore = liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(); + + // Oracle fulfills the request + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + // Verify state changes + assertLt(priorityQueue.totalPendingShares(), pendingSharesBefore, "Pending shares should decrease"); + assertGt(priorityQueue.totalFinalizedShares(), finalizedSharesBefore, "Finalized shares should increase"); + assertGt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked for priority should increase"); + + // Verify request is finalized + assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized"); + } + + function test_fulfillRequests_revertNotMatured() public { + uint128 withdrawAmount = 10 ether; + + // Update config to require maturity time + vm.prank(alice); + priorityQueue.updateWithdrawConfig(1 days, 1 days, 0.01 ether); + + // Create request + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Try to fulfill immediately (should fail - not matured) + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(oracle); + vm.expectRevert(PriorityWithdrawalQueue.NotMatured.selector); + priorityQueue.fulfillRequests(requests); + + // Warp time and try again + vm.warp(block.timestamp + 1 days + 1); + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized after maturity"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ CLAIM TESTS ----------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_claimWithdraw() public { + uint128 withdrawAmount = 10 ether; + + // Setup: VIP user creates a withdrawal request + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Oracle fulfills the request + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + // Record state before claim + uint256 userEthBefore = vipUser.balance; + uint256 finalizedSharesBefore = priorityQueue.totalFinalizedShares(); + uint256 queueEethBefore = eETHInstance.balanceOf(address(priorityQueue)); + + // VIP user claims their ETH + vm.prank(vipUser); + priorityQueue.claimWithdraw(request); + + // Verify state changes + assertLt(priorityQueue.totalFinalizedShares(), finalizedSharesBefore, "Finalized shares should decrease"); + + // Verify ETH was received (approximately, due to share price) + assertApproxEqRel(vipUser.balance, userEthBefore + withdrawAmount, 0.001e18, "User should receive ETH"); + + // Verify eETH was burned from queue + assertLt(eETHInstance.balanceOf(address(priorityQueue)), queueEethBefore, "Queue eETH balance should decrease"); + + // Verify request was removed + bytes32 requestId = keccak256(abi.encode(request)); + assertFalse(priorityQueue.requestExists(requestId), "Request should be removed"); + } + + function test_batchClaimWithdraw() public { + uint128 amount1 = 5 ether; + uint128 amount2 = 3 ether; + + // Create two requests + (, IPriorityWithdrawalQueue.WithdrawRequest memory request1) = + _createWithdrawRequest(vipUser, amount1, DEFAULT_DEADLINE); + (, IPriorityWithdrawalQueue.WithdrawRequest memory request2) = + _createWithdrawRequest(vipUser, amount2, DEFAULT_DEADLINE); + + // Fulfill both + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](2); + requests[0] = request1; + requests[1] = request2; + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + // Batch claim + uint256 ethBefore = vipUser.balance; + vm.prank(vipUser); + priorityQueue.batchClaimWithdraw(requests); + + // Verify ETH received + assertApproxEqRel(vipUser.balance, ethBefore + amount1 + amount2, 0.001e18, "All ETH should be received"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ CANCEL TESTS ---------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_cancelWithdraw() public { + uint128 withdrawAmount = 10 ether; + + // Create request + uint256 eethBefore = eETHInstance.balanceOf(vipUser); + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + uint256 eethAfterRequest = eETHInstance.balanceOf(vipUser); + + // Cancel request + vm.prank(vipUser); + bytes32 cancelledId = priorityQueue.cancelWithdraw(request); + + // Verify + assertEq(cancelledId, requestId, "Cancelled ID should match"); + assertFalse(priorityQueue.requestExists(requestId), "Request should be removed"); + assertEq(eETHInstance.balanceOf(vipUser), eethBefore, "eETH should be returned"); + } + + function test_replaceWithdraw() public { + uint128 withdrawAmount = 10 ether; + uint24 newDeadline = 14 days; + + // Create initial request + (bytes32 oldRequestId, IPriorityWithdrawalQueue.WithdrawRequest memory oldRequest) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Replace with new deadline + vm.prank(vipUser); + (bytes32 returnedOldId, bytes32 newRequestId) = priorityQueue.replaceWithdraw(oldRequest, newDeadline); + + // Verify + assertEq(returnedOldId, oldRequestId, "Old ID should match"); + assertFalse(priorityQueue.requestExists(oldRequestId), "Old request should be removed"); + assertTrue(priorityQueue.requestExists(newRequestId), "New request should exist"); + assertTrue(newRequestId != oldRequestId, "New ID should be different"); + } + + function test_adminCancelUserWithdraws() public { + uint128 withdrawAmount = 10 ether; + + // Create request + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + uint256 eethBefore = eETHInstance.balanceOf(vipUser); + + // Admin cancels + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(alice); + bytes32[] memory cancelledIds = priorityQueue.cancelUserWithdraws(requests); + + // Verify + assertEq(cancelledIds.length, 1, "Should cancel one request"); + assertEq(eETHInstance.balanceOf(vipUser), eethBefore + withdrawAmount, "eETH should be returned"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ FULL FLOW TESTS ------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_fullWithdrawalFlow() public { + // This test verifies the complete flow from deposit to withdrawal + uint128 withdrawAmount = 5 ether; + + // 1. VIP user already has eETH from setUp + uint256 initialEethBalance = eETHInstance.balanceOf(vipUser); + uint256 initialEthBalance = vipUser.balance; + + // 2. Request withdrawal + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Verify intermediate state + assertEq(eETHInstance.balanceOf(vipUser), initialEethBalance - withdrawAmount, "eETH transferred to queue"); + assertGt(priorityQueue.totalPendingShares(), 0, "Pending shares tracked"); + + // 3. Oracle fulfills the request + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + // Verify fulfilled state + assertEq(priorityQueue.totalPendingShares(), 0, "No pending shares after fulfill"); + assertGt(priorityQueue.totalFinalizedShares(), 0, "Shares finalized"); + assertGt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), 0, "LP tracks locked amount"); + + // 4. VIP user claims ETH + vm.prank(vipUser); + priorityQueue.claimWithdraw(request); + + // Verify final state + assertEq(priorityQueue.totalPendingShares(), 0, "No pending"); + assertEq(priorityQueue.totalFinalizedShares(), 0, "No finalized"); + assertApproxEqRel(vipUser.balance, initialEthBalance + withdrawAmount, 0.001e18, "ETH received"); + } + + function test_multipleRequests() public { + uint128 amount1 = 5 ether; + uint128 amount2 = 3 ether; + + // Create two requests + (, IPriorityWithdrawalQueue.WithdrawRequest memory request1) = + _createWithdrawRequest(vipUser, amount1, DEFAULT_DEADLINE); + (, IPriorityWithdrawalQueue.WithdrawRequest memory request2) = + _createWithdrawRequest(vipUser, amount2, DEFAULT_DEADLINE); + + // Verify both requests tracked + assertEq(priorityQueue.totalActiveRequests(), 2, "Should have 2 active requests"); + assertEq(priorityQueue.nonce(), 3, "Nonce should be 3"); + + // Fulfill both at once + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](2); + requests[0] = request1; + requests[1] = request2; + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + // Verify both fulfilled + assertEq(priorityQueue.totalPendingShares(), 0, "No pending after fulfill"); + assertGt(priorityQueue.totalFinalizedShares(), 0, "Shares finalized"); + + // Claim both + uint256 ethBefore = vipUser.balance; + vm.startPrank(vipUser); + priorityQueue.claimWithdraw(request1); + priorityQueue.claimWithdraw(request2); + vm.stopPrank(); + + // Verify final state + assertEq(priorityQueue.totalFinalizedShares(), 0, "All claimed"); + assertApproxEqRel(vipUser.balance, ethBefore + amount1 + amount2, 0.001e18, "All ETH received"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ WHITELIST TESTS ------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_whitelistManagement() public { + address newUser = makeAddr("newUser"); + + // Initially not whitelisted + assertFalse(priorityQueue.isWhitelisted(newUser), "Should not be whitelisted initially"); + + // Admin adds to whitelist + vm.prank(alice); + priorityQueue.addToWhitelist(newUser); + assertTrue(priorityQueue.isWhitelisted(newUser), "Should be whitelisted after add"); + + // Admin removes from whitelist + vm.prank(alice); + priorityQueue.removeFromWhitelist(newUser); + assertFalse(priorityQueue.isWhitelisted(newUser), "Should not be whitelisted after remove"); + } + + function test_batchUpdateWhitelist() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + address[] memory users = new address[](2); + users[0] = user1; + users[1] = user2; + bool[] memory statuses = new bool[](2); + statuses[0] = true; + statuses[1] = true; + + vm.prank(alice); + priorityQueue.batchUpdateWhitelist(users, statuses); + + assertTrue(priorityQueue.isWhitelisted(user1), "User1 should be whitelisted"); + assertTrue(priorityQueue.isWhitelisted(user2), "User2 should be whitelisted"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ INVALIDATION TESTS ---------------------------------- + //-------------------------------------------------------------------------------------- + + function test_invalidateRequest() public { + uint128 withdrawAmount = 10 ether; + + // Create request + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + + // Admin invalidates + vm.prank(alice); + priorityQueue.invalidateRequest(request); + + // Verify invalidated + assertTrue(priorityQueue.invalidatedRequests(requestId), "Request should be invalidated"); + + // Oracle cannot fulfill invalidated request + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + vm.prank(oracle); + vm.expectRevert(PriorityWithdrawalQueue.RequestInvalidated.selector); + priorityQueue.fulfillRequests(requests); + } + + function test_validateRequest() public { + uint128 withdrawAmount = 10 ether; + + // Create and invalidate request + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + vm.prank(alice); + priorityQueue.invalidateRequest(request); + + // Re-validate + vm.prank(alice); + priorityQueue.validateRequest(requestId); + + // Verify no longer invalidated + assertFalse(priorityQueue.invalidatedRequests(requestId), "Request should not be invalidated"); + + // Oracle can now fulfill + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ CONFIG TESTS ---------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_updateWithdrawConfig() public { + uint24 newMaturity = 12 hours; + uint24 newMinDeadline = 2 days; + uint96 newMinAmount = 1 ether; + + vm.prank(alice); + priorityQueue.updateWithdrawConfig(newMaturity, newMinDeadline, newMinAmount); + + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + assertEq(config.secondsToMaturity, newMaturity, "Maturity should be updated"); + assertEq(config.minimumSecondsToDeadline, newMinDeadline, "Min deadline should be updated"); + assertEq(config.minimumAmount, newMinAmount, "Min amount should be updated"); + } + + function test_setWithdrawCapacity() public { + uint256 newCapacity = 100 ether; + + vm.prank(alice); + priorityQueue.setWithdrawCapacity(newCapacity); + + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + assertEq(config.withdrawCapacity, newCapacity, "Capacity should be updated"); + } + + function test_stopWithdraws() public { + vm.prank(alice); + priorityQueue.stopWithdraws(); + + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + assertFalse(config.allowWithdraws, "Withdraws should be stopped"); + + // Cannot create new requests + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), 1 ether); + vm.expectRevert(PriorityWithdrawalQueue.WithdrawsNotAllowed.selector); + priorityQueue.requestWithdraw(1 ether, DEFAULT_DEADLINE); + vm.stopPrank(); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ REVERT TESTS ---------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_revert_notWhitelisted() public { + vm.deal(regularUser, 10 ether); + vm.startPrank(regularUser); + liquidityPoolInstance.deposit{value: 5 ether}(); + eETHInstance.approve(address(priorityQueue), 1 ether); + + vm.expectRevert(PriorityWithdrawalQueue.NotWhitelisted.selector); + priorityQueue.requestWithdraw(1 ether, DEFAULT_DEADLINE); + vm.stopPrank(); + } + + function test_revert_claimNotFinalized() public { + // Create request but don't fulfill + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + + vm.prank(vipUser); + vm.expectRevert(PriorityWithdrawalQueue.RequestNotFinalized.selector); + priorityQueue.claimWithdraw(request); + } + + function test_revert_claimWrongOwner() public { + // VIP creates request + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + + // Fulfill + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + vm.prank(oracle); + priorityQueue.fulfillRequests(requests); + + // Another user tries to claim + vm.prank(regularUser); + vm.expectRevert(PriorityWithdrawalQueue.NotRequestOwner.selector); + priorityQueue.claimWithdraw(request); + } + + function test_revert_fulfillNonOracle() public { + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(regularUser); + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.fulfillRequests(requests); + } + + function test_revert_cancelWrongOwner() public { + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + + vm.prank(regularUser); + vm.expectRevert(PriorityWithdrawalQueue.NotRequestOwner.selector); + priorityQueue.cancelWithdraw(request); + } + + function test_revert_deadlineTooShort() public { + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), 1 ether); + + // Default minimum deadline is 1 day + vm.expectRevert(PriorityWithdrawalQueue.InvalidDeadline.selector); + priorityQueue.requestWithdraw(1 ether, 1 hours); + vm.stopPrank(); + } + + function test_revert_amountTooSmall() public { + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), 0.001 ether); + + // Default minimum amount is 0.01 ether + vm.expectRevert(PriorityWithdrawalQueue.InvalidAmount.selector); + priorityQueue.requestWithdraw(0.001 ether, DEFAULT_DEADLINE); + vm.stopPrank(); + } + + function test_revert_notEnoughCapacity() public { + // Set low capacity + vm.prank(alice); + priorityQueue.setWithdrawCapacity(1 ether); + + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), 10 ether); + + vm.expectRevert(PriorityWithdrawalQueue.NotEnoughWithdrawCapacity.selector); + priorityQueue.requestWithdraw(10 ether, DEFAULT_DEADLINE); + vm.stopPrank(); + } +} From 7811d337eed40956c3b036d5d8a2deaec868baff Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Tue, 27 Jan 2026 20:02:23 -0500 Subject: [PATCH 02/19] refactor: Update PriorityWithdrawalQueue contract to improve withdrawal request handling and treasury fee distribution - Rename and adjust constants for clarity - Introduce immutable treasury address and share remainder split to treasury - Simplify withdrawal request structure by removing unnecessary parameters - Enhance request management with new roles and functions for invalidating requests - Update event emissions and error handling for better clarity and functionality --- src/PriorityWithdrawalQueue.sol | 393 ++++++++------------ src/interfaces/IPriorityWithdrawalQueue.sol | 33 +- 2 files changed, 172 insertions(+), 254 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index c7ca4e598..580d98f9d 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -32,19 +32,26 @@ contract PriorityWithdrawalQueue is //--------------------------------- CONSTANTS ---------------------------------------- //-------------------------------------------------------------------------------------- - /// @notice Maximum time in seconds a withdraw request can take to mature - uint24 public constant MAXIMUM_SECONDS_TO_MATURITY = 30 days; + /// @notice Maximum delay in seconds before a request can be fulfilled + uint24 public constant MAXIMUM_MIN_DELAY = 30 days; - /// @notice Maximum minimum validity period after maturity - uint24 public constant MAXIMUM_MINIMUM_SECONDS_TO_DEADLINE = 30 days; + /// @notice Basis point scale for fee calculations (100% = 10000) + uint256 private constant _BASIS_POINT_SCALE = 1e4; //-------------------------------------------------------------------------------------- - //--------------------------------- STATE-VARIABLES ---------------------------------- + //--------------------------------- IMMUTABLES --------------------------------------- //-------------------------------------------------------------------------------------- - ILiquidityPool public liquidityPool; - IeETH public eETH; - IRoleRegistry public roleRegistry; + ILiquidityPool public immutable liquidityPool; + IeETH public immutable eETH; + IRoleRegistry public immutable roleRegistry; + + /// @notice Treasury address for fee collection + address public immutable treasury; + + //-------------------------------------------------------------------------------------- + //--------------------------------- STATE-VARIABLES ---------------------------------- + //-------------------------------------------------------------------------------------- /// @notice EnumerableSet to store all active withdraw request IDs EnumerableSet.Bytes32Set private _withdrawRequests; @@ -64,15 +71,12 @@ contract PriorityWithdrawalQueue is /// @notice Request nonce to prevent hash collisions uint96 public nonce; - /// @notice Total eETH shares held for pending requests - uint256 public totalPendingShares; - - /// @notice Total eETH shares held for finalized (claimable) requests - uint256 public totalFinalizedShares; - /// @notice Remainder shares from claimed withdrawals (difference between request shares and actual burned) uint256 public totalRemainderShares; + /// @notice Fee split to treasury in basis points (e.g., 5000 = 50%) + uint16 public shareRemainderSplitToTreasuryInBps; + /// @notice Contract pause state bool public paused; @@ -81,7 +85,8 @@ contract PriorityWithdrawalQueue is //-------------------------------------------------------------------------------------- bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE"); - bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE"); + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE"); + bytes32 public constant IMPLICIT_FEE_CLAIMER_ROLE = keccak256("IMPLICIT_FEE_CLAIMER_ROLE"); //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- @@ -95,20 +100,17 @@ contract PriorityWithdrawalQueue is uint96 nonce, uint128 amountOfEEth, uint128 shareOfEEth, - uint40 creationTime, - uint24 secondsToMaturity, - uint24 secondsToDeadline + uint40 creationTime ); event WithdrawRequestCancelled(bytes32 indexed requestId, address indexed user, uint256 timestamp); event WithdrawRequestFinalized(bytes32 indexed requestId, address indexed user, uint256 timestamp); event WithdrawRequestClaimed(bytes32 indexed requestId, address indexed user, uint256 amountClaimed, uint256 sharesBurned); event WithdrawRequestInvalidated(bytes32 indexed requestId); - event WithdrawRequestValidated(bytes32 indexed requestId); event WhitelistUpdated(address indexed user, bool status); - event WithdrawConfigUpdated(uint24 secondsToMaturity, uint24 minimumSecondsToDeadline, uint96 minimumAmount); + event WithdrawConfigUpdated(uint24 minDelay, uint96 minimumAmount); event WithdrawCapacityUpdated(uint256 withdrawCapacity); - event WithdrawsStopped(); - event RemainderHandled(uint256 remainderAmount, uint256 remainderShares); + event RemainderHandled(uint256 amountToTreasury, uint256 amountBurned); + event ShareRemainderSplitUpdated(uint16 newSplitInBps); //-------------------------------------------------------------------------------------- //------------------------------------- ERRORS --------------------------------------- @@ -116,7 +118,6 @@ contract PriorityWithdrawalQueue is error NotWhitelisted(); error InvalidAmount(); - error InvalidDeadline(); error RequestNotFound(); error RequestNotFinalized(); error RequestInvalidated(); @@ -125,14 +126,15 @@ contract PriorityWithdrawalQueue is error IncorrectRole(); error ContractPaused(); error ContractNotPaused(); - error WithdrawsNotAllowed(); error NotEnoughWithdrawCapacity(); error NotMatured(); - error DeadlinePassed(); error Keccak256Collision(); error InvalidConfig(); error PermitFailedAndAllowanceTooLow(); + error ArrayLengthMismatch(); + error AddressZero(); error BadInput(); + error InvalidBurnedSharesAmount(); //-------------------------------------------------------------------------------------- //----------------------------------- MODIFIERS -------------------------------------- @@ -153,8 +155,8 @@ contract PriorityWithdrawalQueue is _; } - modifier onlyOracle() { - if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE, msg.sender)) revert IncorrectRole(); + modifier onlyRequestManager() { + if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); _; } @@ -168,41 +170,32 @@ contract PriorityWithdrawalQueue is //-------------------------------------------------------------------------------------- /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + constructor(address _liquidityPool, address _eETH, address _roleRegistry, address _treasury) { + if (_liquidityPool == address(0) || _eETH == address(0) || _roleRegistry == address(0) || _treasury == address(0)) { + revert AddressZero(); + } + + liquidityPool = ILiquidityPool(_liquidityPool); + eETH = IeETH(_eETH); + roleRegistry = IRoleRegistry(_roleRegistry); + treasury = _treasury; + _disableInitializers(); } /// @notice Initialize the contract - /// @param _liquidityPool Address of the LiquidityPool contract - /// @param _eETH Address of the eETH contract - /// @param _roleRegistry Address of the RoleRegistry contract - function initialize( - address _liquidityPool, - address _eETH, - address _roleRegistry - ) external initializer { - if (_liquidityPool == address(0) || _eETH == address(0) || _roleRegistry == address(0)) { - revert BadInput(); - } - + function initialize() external initializer { __Ownable_init(); __UUPSUpgradeable_init(); __ReentrancyGuard_init(); - liquidityPool = ILiquidityPool(_liquidityPool); - eETH = IeETH(_eETH); - roleRegistry = IRoleRegistry(_roleRegistry); - nonce = 1; - paused = false; - // Default config - can be updated by admin _withdrawConfig = WithdrawConfig({ - allowWithdraws: true, - secondsToMaturity: 0, // Instant maturity by default for priority users - minimumSecondsToDeadline: 1 days, + minDelay: 0, + creationTime: uint40(block.timestamp), minimumAmount: 0.01 ether, - withdrawCapacity: type(uint256).max + withdrawCapacity: 10_000_000 ether }); } @@ -212,44 +205,34 @@ contract PriorityWithdrawalQueue is /// @notice Request a withdrawal of eETH /// @param amountOfEEth Amount of eETH to withdraw - /// @param secondsToDeadline Time in seconds the request is valid for after maturity /// @return requestId The hash-based ID of the created withdrawal request function requestWithdraw( - uint128 amountOfEEth, - uint24 secondsToDeadline + uint128 amountOfEEth ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { _decrementWithdrawCapacity(amountOfEEth); - _validateNewRequest(amountOfEEth, secondsToDeadline); + if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); - (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth, secondsToDeadline); + (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth); } /// @notice Request a withdrawal with permit for gasless approval /// @param amountOfEEth Amount of eETH to withdraw - /// @param secondsToDeadline Time in seconds the request is valid for after maturity /// @param permit Permit signature data for eETH approval /// @return requestId The hash-based ID of the created withdrawal request function requestWithdrawWithPermit( uint128 amountOfEEth, - uint24 secondsToDeadline, PermitInput calldata permit ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { _decrementWithdrawCapacity(amountOfEEth); - _validateNewRequest(amountOfEEth, secondsToDeadline); - - // Try permit - continue if it fails (may already be approved) - try eETH.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} - catch { - if (IERC20(address(eETH)).allowance(msg.sender, address(this)) < amountOfEEth) { - revert PermitFailedAndAllowanceTooLow(); - } - } + if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); + + try eETH.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); - (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth, secondsToDeadline); + (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth); } /// @notice Cancel a pending withdrawal request @@ -261,48 +244,28 @@ contract PriorityWithdrawalQueue is requestId = _cancelWithdrawRequest(request); } - /// @notice Replace an existing withdrawal request with new parameters - /// @param oldRequest The existing request to replace - /// @param newSecondsToDeadline New validity period for the replacement request - /// @return oldRequestId The cancelled request ID - /// @return newRequestId The new request ID - function replaceWithdraw( - WithdrawRequest calldata oldRequest, - uint24 newSecondsToDeadline - ) external whenNotPaused onlyRequestUser(oldRequest.user) returns (bytes32 oldRequestId, bytes32 newRequestId) { - _validateNewRequest(oldRequest.amountOfEEth, newSecondsToDeadline); - - // Dequeue old request (no capacity increment since we're replacing) - oldRequestId = _dequeueWithdrawRequest(oldRequest); - - emit WithdrawRequestCancelled(oldRequestId, oldRequest.user, block.timestamp); - - // Queue new request with same amount (no capacity decrement) - (newRequestId,) = _queueWithdrawRequest(oldRequest.user, oldRequest.amountOfEEth, newSecondsToDeadline); - } - /// @notice Claim ETH for a finalized withdrawal request /// @param request The withdrawal request to claim function claimWithdraw(WithdrawRequest calldata request) external whenNotPaused nonReentrant { - _claimWithdraw(request, request.user); + _claimWithdraw(request); } /// @notice Batch claim multiple withdrawal requests /// @param requests Array of withdrawal requests to claim function batchClaimWithdraw(WithdrawRequest[] calldata requests) external whenNotPaused nonReentrant { for (uint256 i = 0; i < requests.length; ++i) { - _claimWithdraw(requests[i], requests[i].user); + _claimWithdraw(requests[i]); } } //-------------------------------------------------------------------------------------- - //---------------------------- ORACLE/SOLVER FUNCTIONS ------------------------------- + //---------------------------- REQUEST MANAGER FUNCTIONS ------------------------------ //-------------------------------------------------------------------------------------- - /// @notice Oracle finalizes withdrawal requests after maturity + /// @notice Request manager finalizes withdrawal requests after maturity /// @dev Checks maturity and deadline, marks requests as finalized /// @param requests Array of requests to finalize - function fulfillRequests(WithdrawRequest[] calldata requests) external onlyOracle whenNotPaused { + function fulfillRequests(WithdrawRequest[] calldata requests) external onlyRequestManager whenNotPaused { uint256 totalSharesToFinalize = 0; for (uint256 i = 0; i < requests.length; ++i) { @@ -311,20 +274,12 @@ contract PriorityWithdrawalQueue is // Verify request exists in pending set if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); - - // Check not already finalized if (_finalizedRequests.contains(requestId)) revert RequestAlreadyFinalized(); - - // Check not invalidated if (invalidatedRequests[requestId]) revert RequestInvalidated(); - // Check maturity - uint256 maturity = request.creationTime + request.secondsToMaturity; - if (block.timestamp < maturity) revert NotMatured(); - - // Check deadline - uint256 deadline = maturity + request.secondsToDeadline; - if (block.timestamp > deadline) revert DeadlinePassed(); + // Check minDelay has passed (request must wait at least minDelay seconds) + uint256 earliestFulfillTime = request.creationTime + _withdrawConfig.minDelay; + if (block.timestamp < earliestFulfillTime) revert NotMatured(); // Add to finalized set _finalizedRequests.add(requestId); @@ -333,10 +288,6 @@ contract PriorityWithdrawalQueue is emit WithdrawRequestFinalized(requestId, request.user, block.timestamp); } - // Update accounting - totalPendingShares -= totalSharesToFinalize; - totalFinalizedShares += totalSharesToFinalize; - // Lock ETH in LiquidityPool for priority withdrawals uint256 totalAmountToLock = liquidityPool.amountForShare(totalSharesToFinalize); liquidityPool.addEthAmountLockedForPriorityWithdrawal(uint128(totalAmountToLock)); @@ -349,7 +300,7 @@ contract PriorityWithdrawalQueue is /// @notice Add an address to the whitelist /// @param user Address to whitelist function addToWhitelist(address user) external onlyAdmin { - if (user == address(0)) revert BadInput(); + if (user == address(0)) revert AddressZero(); isWhitelisted[user] = true; emit WhitelistUpdated(user, true); } @@ -365,32 +316,28 @@ contract PriorityWithdrawalQueue is /// @param users Array of user addresses /// @param statuses Array of whitelist statuses function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external onlyAdmin { - if (users.length != statuses.length) revert BadInput(); + if (users.length != statuses.length) revert ArrayLengthMismatch(); for (uint256 i = 0; i < users.length; ++i) { - if (users[i] == address(0)) revert BadInput(); + if (users[i] == address(0)) revert AddressZero(); isWhitelisted[users[i]] = statuses[i]; emit WhitelistUpdated(users[i], statuses[i]); } } /// @notice Update withdrawal configuration - /// @param secondsToMaturity Time until requests can be fulfilled - /// @param minimumSecondsToDeadline Minimum validity period after maturity + /// @param minDelay Minimum delay before requests can be fulfilled /// @param minimumAmount Minimum withdrawal amount function updateWithdrawConfig( - uint24 secondsToMaturity, - uint24 minimumSecondsToDeadline, + uint24 minDelay, uint96 minimumAmount ) external onlyAdmin { - if (secondsToMaturity > MAXIMUM_SECONDS_TO_MATURITY) revert InvalidConfig(); - if (minimumSecondsToDeadline > MAXIMUM_MINIMUM_SECONDS_TO_DEADLINE) revert InvalidConfig(); + if (minDelay > MAXIMUM_MIN_DELAY) revert InvalidConfig(); - _withdrawConfig.secondsToMaturity = secondsToMaturity; - _withdrawConfig.minimumSecondsToDeadline = minimumSecondsToDeadline; + _withdrawConfig.minDelay = minDelay; + _withdrawConfig.creationTime = uint40(block.timestamp); _withdrawConfig.minimumAmount = minimumAmount; - _withdrawConfig.allowWithdraws = true; - emit WithdrawConfigUpdated(secondsToMaturity, minimumSecondsToDeadline, minimumAmount); + emit WithdrawConfigUpdated(minDelay, minimumAmount); } /// @notice Set the withdrawal capacity @@ -400,71 +347,56 @@ contract PriorityWithdrawalQueue is emit WithdrawCapacityUpdated(capacity); } - /// @notice Stop all withdrawals - function stopWithdraws() external onlyAdmin { - _withdrawConfig.allowWithdraws = false; - emit WithdrawsStopped(); - } - /// @notice Invalidate a withdrawal request (prevents finalization) - /// @param request The request to invalidate - function invalidateRequest(WithdrawRequest calldata request) external onlyAdmin { - bytes32 requestId = keccak256(abi.encode(request)); - if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); - if (invalidatedRequests[requestId]) revert RequestInvalidated(); - - invalidatedRequests[requestId] = true; - emit WithdrawRequestInvalidated(requestId); - } - - /// @notice Validate a previously invalidated request - /// @param requestId The request ID to validate - function validateRequest(bytes32 requestId) external onlyAdmin { - if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); - if (!invalidatedRequests[requestId]) revert BadInput(); - - invalidatedRequests[requestId] = false; - emit WithdrawRequestValidated(requestId); - } - - /// @notice Bulk finalize requests up to a certain request ID - /// @dev Used for batch finalization by admin - /// @param upToRequestId The request ID to finalize up to - function finalizeRequests(bytes32 upToRequestId) external onlyAdmin { - // This function allows admin to mark a specific request as finalized - // Useful for edge cases where oracle flow is bypassed - if (!_withdrawRequests.contains(upToRequestId)) revert RequestNotFound(); - if (_finalizedRequests.contains(upToRequestId)) revert RequestAlreadyFinalized(); - - _finalizedRequests.add(upToRequestId); - } + /// @param requests Array of requests to invalidate + /// @return invalidatedRequestIds Array of request IDs that were invalidated + function invalidateRequests(WithdrawRequest[] calldata requests) external onlyRequestManager returns (bytes32[] memory invalidatedRequestIds) { + invalidatedRequestIds = new bytes32[](requests.length); + for (uint256 i = 0; i < requests.length; ++i) { + bytes32 requestId = keccak256(abi.encode(requests[i])); + if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + if (invalidatedRequests[requestId]) revert RequestInvalidated(); - /// @notice Admin cancel multiple user withdrawals - /// @param requests Array of requests to cancel - /// @return cancelledRequestIds Array of cancelled request IDs - function cancelUserWithdraws( - WithdrawRequest[] calldata requests - ) external onlyAdmin returns (bytes32[] memory cancelledRequestIds) { - uint256 length = requests.length; - cancelledRequestIds = new bytes32[](length); - - for (uint256 i = 0; i < length; ++i) { - cancelledRequestIds[i] = _cancelWithdrawRequest(requests[i]); + _cancelWithdrawRequest(requests[i]); + invalidatedRequestIds[i] = requestId; + invalidatedRequests[requestId] = true; + emit WithdrawRequestInvalidated(requestId); } } /// @notice Handle remainder shares (from rounding differences) - /// @param sharesToBurn Amount of remainder shares to burn - function handleRemainder(uint256 sharesToBurn) external onlyAdmin { - if (sharesToBurn > totalRemainderShares) revert BadInput(); - - uint256 amountToBurn = liquidityPool.amountForShare(sharesToBurn); - totalRemainderShares -= sharesToBurn; - - // Burn the remainder - liquidityPool.burnEEthShares(sharesToBurn); - - emit RemainderHandled(amountToBurn, sharesToBurn); + /// @dev Splits the remainder into two parts: + /// - Treasury: gets a percentage of the remainder based on shareRemainderSplitToTreasuryInBps + /// - Burn: the rest of the remainder is burned + /// @param eEthAmount Amount of eETH remainder to handle + function handleRemainder(uint256 eEthAmount) external { + if (!roleRegistry.hasRole(IMPLICIT_FEE_CLAIMER_ROLE, msg.sender)) revert IncorrectRole(); + if (eEthAmount == 0) revert BadInput(); + if (eEthAmount > liquidityPool.amountForShare(totalRemainderShares)) revert BadInput(); + + uint256 beforeEEthShares = eETH.shares(address(this)); + + uint256 eEthAmountToTreasury = eEthAmount.mulDiv(shareRemainderSplitToTreasuryInBps, _BASIS_POINT_SCALE); + uint256 eEthAmountToBurn = eEthAmount - eEthAmountToTreasury; + uint256 eEthSharesToBurn = liquidityPool.sharesForAmount(eEthAmountToBurn); + uint256 eEthSharesMoved = eEthSharesToBurn + liquidityPool.sharesForAmount(eEthAmountToTreasury); + + totalRemainderShares -= eEthSharesMoved; + + if (eEthAmountToTreasury > 0) IERC20(address(eETH)).safeTransfer(treasury, eEthAmountToTreasury); + if (eEthSharesToBurn > 0) liquidityPool.burnEEthShares(eEthSharesToBurn); + + require(beforeEEthShares - eEthSharesMoved == eETH.shares(address(this)), "Invalid eETH shares after remainder handling"); + + emit RemainderHandled(eEthAmountToTreasury, liquidityPool.amountForShare(eEthSharesToBurn)); + } + + /// @notice Update the share remainder split to treasury + /// @param _shareRemainderSplitToTreasuryInBps New split percentage in basis points (max 10000) + function updateShareRemainderSplitToTreasury(uint16 _shareRemainderSplitToTreasuryInBps) external onlyAdmin { + if (_shareRemainderSplitToTreasuryInBps > _BASIS_POINT_SCALE) revert InvalidConfig(); + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; + emit ShareRemainderSplitUpdated(_shareRemainderSplitToTreasuryInBps); } /// @notice Pause the contract @@ -487,13 +419,6 @@ contract PriorityWithdrawalQueue is //------------------------------ INTERNAL FUNCTIONS ---------------------------------- //-------------------------------------------------------------------------------------- - /// @dev Validate new request parameters - function _validateNewRequest(uint128 amountOfEEth, uint24 secondsToDeadline) internal view { - if (!_withdrawConfig.allowWithdraws) revert WithdrawsNotAllowed(); - if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); - if (secondsToDeadline < _withdrawConfig.minimumSecondsToDeadline) revert InvalidDeadline(); - } - /// @dev Decrement withdrawal capacity function _decrementWithdrawCapacity(uint128 amount) internal { if (_withdrawConfig.withdrawCapacity < type(uint256).max) { @@ -514,8 +439,7 @@ contract PriorityWithdrawalQueue is /// @dev Queue a new withdrawal request function _queueWithdrawRequest( address user, - uint128 amountOfEEth, - uint24 secondsToDeadline + uint128 amountOfEEth ) internal returns (bytes32 requestId, WithdrawRequest memory req) { uint96 requestNonce; unchecked { @@ -532,9 +456,7 @@ contract PriorityWithdrawalQueue is user: user, amountOfEEth: amountOfEEth, shareOfEEth: shareOfEEth, - creationTime: timeNow, - secondsToMaturity: _withdrawConfig.secondsToMaturity, - secondsToDeadline: secondsToDeadline + creationTime: timeNow }); requestId = keccak256(abi.encode(req)); @@ -542,17 +464,13 @@ contract PriorityWithdrawalQueue is bool addedToSet = _withdrawRequests.add(requestId); if (!addedToSet) revert Keccak256Collision(); - totalPendingShares += shareOfEEth; - emit WithdrawRequestCreated( requestId, user, requestNonce, amountOfEEth, shareOfEEth, - timeNow, - _withdrawConfig.secondsToMaturity, - secondsToDeadline + timeNow ); } @@ -561,50 +479,42 @@ contract PriorityWithdrawalQueue is requestId = keccak256(abi.encode(request)); bool removedFromSet = _withdrawRequests.remove(requestId); if (!removedFromSet) revert RequestNotFound(); - - // Also remove from finalized if it was there + _finalizedRequests.remove(requestId); } /// @dev Cancel a withdrawal request and return eETH to user function _cancelWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { - requestId = _dequeueWithdrawRequest(request); + requestId = keccak256(abi.encode(request)); - // Update accounting based on whether it was finalized - if (_finalizedRequests.contains(requestId)) { - totalFinalizedShares -= request.shareOfEEth; - // Unlock ETH from LiquidityPool + // Check if finalized BEFORE dequeue (dequeue removes from finalized set) + bool wasFinalized = _finalizedRequests.contains(requestId); + + _dequeueWithdrawRequest(request); + + // Unlock ETH from LiquidityPool if it was finalized + if (wasFinalized) { uint256 amountToUnlock = liquidityPool.amountForShare(request.shareOfEEth); liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToUnlock)); - } else { - totalPendingShares -= request.shareOfEEth; } _incrementWithdrawCapacity(request.amountOfEEth); - // Return eETH to user IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); emit WithdrawRequestCancelled(requestId, request.user, block.timestamp); } /// @dev Internal claim function - function _claimWithdraw(WithdrawRequest calldata request, address recipient) internal { + function _claimWithdraw(WithdrawRequest calldata request) internal { if (request.user != msg.sender) revert NotRequestOwner(); bytes32 requestId = keccak256(abi.encode(request)); - // Verify request exists if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); - - // Verify request is finalized if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); - - // Verify not invalidated if (invalidatedRequests[requestId]) revert RequestInvalidated(); - // Calculate claimable amount (min of requested amount or current share value) - // This protects users if rate has changed unfavorably, and protocol if rate increased uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); uint256 amountToWithdraw = request.amountOfEEth < amountForShares ? request.amountOfEEth @@ -612,7 +522,6 @@ contract PriorityWithdrawalQueue is uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); - // Remove from sets _withdrawRequests.remove(requestId); _finalizedRequests.remove(requestId); @@ -621,15 +530,11 @@ contract PriorityWithdrawalQueue is ? request.shareOfEEth - sharesToBurn : 0; totalRemainderShares += remainder; - - // Update accounting - totalFinalizedShares -= request.shareOfEEth; - // Execute withdrawal through LiquidityPool - uint256 burnedShares = liquidityPool.withdraw(recipient, amountToWithdraw); - assert(burnedShares == sharesToBurn); + uint256 burnedShares = liquidityPool.withdraw(msg.sender, amountToWithdraw); + if (burnedShares != sharesToBurn) revert InvalidBurnedSharesAmount(); - emit WithdrawRequestClaimed(requestId, recipient, amountToWithdraw, burnedShares); + emit WithdrawRequestClaimed(requestId, msg.sender, amountToWithdraw, burnedShares); } function _authorizeUpgrade(address) internal override { @@ -640,11 +545,41 @@ contract PriorityWithdrawalQueue is //------------------------------------ GETTERS --------------------------------------- //-------------------------------------------------------------------------------------- + /// @notice Generate a request ID from individual parameters + /// @param _nonce The request nonce + /// @param _user The user address + /// @param _amountOfEEth The amount of eETH + /// @param _shareOfEEth The share of eETH + /// @param _creationTime The creation timestamp + /// @return requestId The keccak256 hash of the request + function generateWithdrawRequestId( + uint96 _nonce, + address _user, + uint128 _amountOfEEth, + uint128 _shareOfEEth, + uint40 _creationTime + ) public pure returns (bytes32 requestId) { + WithdrawRequest memory req = WithdrawRequest({ + nonce: _nonce, + user: _user, + amountOfEEth: _amountOfEEth, + shareOfEEth: _shareOfEEth, + creationTime: _creationTime + }); + requestId = keccak256(abi.encode(req)); + } + /// @notice Get the request ID from a request struct /// @param request The withdrawal request /// @return requestId The keccak256 hash of the request function getRequestId(WithdrawRequest calldata request) external pure returns (bytes32) { - return keccak256(abi.encode(request)); + return generateWithdrawRequestId( + request.nonce, + request.user, + request.amountOfEEth, + request.shareOfEEth, + request.creationTime + ); } /// @notice Get all active request IDs @@ -697,18 +632,6 @@ contract PriorityWithdrawalQueue is return _withdrawRequests.length(); } - /// @notice Get the total eETH amount pending (not finalized) - /// @return The total pending eETH amount - function totalPendingAmount() external view returns (uint256) { - return liquidityPool.amountForShare(totalPendingShares); - } - - /// @notice Get the total eETH amount finalized (ready for claim) - /// @return The total finalized eETH amount - function totalFinalizedAmount() external view returns (uint256) { - return liquidityPool.amountForShare(totalFinalizedShares); - } - /// @notice Get the total remainder amount available /// @return The total remainder eETH amount function getRemainderAmount() external view returns (uint256) { diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol index 1a3661d3f..9ede989d0 100644 --- a/src/interfaces/IPriorityWithdrawalQueue.sol +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -8,28 +8,22 @@ interface IPriorityWithdrawalQueue { /// @param amountOfEEth Original eETH amount requested /// @param shareOfEEth eETH shares at time of request /// @param creationTime Timestamp when request was created - /// @param secondsToMaturity Time until request can be fulfilled - /// @param secondsToDeadline Time after maturity until request expires struct WithdrawRequest { uint96 nonce; address user; uint128 amountOfEEth; uint128 shareOfEEth; uint40 creationTime; - uint24 secondsToMaturity; - uint24 secondsToDeadline; } /// @notice Configuration for withdrawal parameters - /// @param allowWithdraws Whether withdrawals are currently allowed - /// @param secondsToMaturity Time in seconds until a request can be fulfilled - /// @param minimumSecondsToDeadline Minimum validity period after maturity + /// @param minDelay Minimum delay in seconds before a request can be fulfilled + /// @param creationTime Timestamp when the config was last updated /// @param minimumAmount Minimum eETH amount per withdrawal /// @param withdrawCapacity Maximum pending withdrawal amount allowed struct WithdrawConfig { - bool allowWithdraws; - uint24 secondsToMaturity; - uint24 minimumSecondsToDeadline; + uint24 minDelay; + uint40 creationTime; uint96 minimumAmount; uint256 withdrawCapacity; } @@ -43,10 +37,9 @@ interface IPriorityWithdrawalQueue { } // User functions - function requestWithdraw(uint128 amountOfEEth, uint24 secondsToDeadline) external returns (bytes32 requestId); - function requestWithdrawWithPermit(uint128 amountOfEEth, uint24 secondsToDeadline, PermitInput calldata permit) external returns (bytes32 requestId); + function requestWithdraw(uint128 amountOfEEth) external returns (bytes32 requestId); + function requestWithdrawWithPermit(uint128 amountOfEEth, PermitInput calldata permit) external returns (bytes32 requestId); function cancelWithdraw(WithdrawRequest calldata request) external returns (bytes32 requestId); - function replaceWithdraw(WithdrawRequest calldata oldRequest, uint24 newSecondsToDeadline) external returns (bytes32 oldRequestId, bytes32 newRequestId); function claimWithdraw(WithdrawRequest calldata request) external; function batchClaimWithdraw(WithdrawRequest[] calldata requests) external; @@ -65,13 +58,15 @@ interface IPriorityWithdrawalQueue { function addToWhitelist(address user) external; function removeFromWhitelist(address user) external; function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external; - function updateWithdrawConfig(uint24 secondsToMaturity, uint24 minimumSecondsToDeadline, uint96 minimumAmount) external; + function updateWithdrawConfig(uint24 minDelay, uint96 minimumAmount) external; function setWithdrawCapacity(uint256 capacity) external; - function stopWithdraws() external; - function invalidateRequest(WithdrawRequest calldata request) external; - function validateRequest(bytes32 requestId) external; - function finalizeRequests(bytes32 upToRequestId) external; - function cancelUserWithdraws(WithdrawRequest[] calldata requests) external returns (bytes32[] memory); + function invalidateRequests(WithdrawRequest[] calldata requests) external returns(bytes32[] memory); + function updateShareRemainderSplitToTreasury(uint16 _shareRemainderSplitToTreasuryInBps) external; + function handleRemainder(uint256 eEthAmount) external; function pauseContract() external; function unPauseContract() external; + + // Immutables + function treasury() external view returns (address); + function shareRemainderSplitToTreasuryInBps() external view returns (uint16); } From 73b0ed4331b679c70142893f2d04eb45d9d1e200 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Tue, 27 Jan 2026 20:02:34 -0500 Subject: [PATCH 03/19] refactor: Update LiquidityPool contract to use immutable priorityWithdrawalQueue and remove setter function - Change priorityWithdrawalQueue to an immutable variable initialized in the constructor - Remove the setPriorityWithdrawalQueue function to enhance security and restrict modifications - Adjust reduceEthAmountLockedForPriorityWithdrawal to allow only the priorityWithdrawalQueue to call it --- src/LiquidityPool.sol | 23 ++++++++++++----------- src/interfaces/ILiquidityPool.sol | 1 - 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index c345bc26b..c1bfd6cbb 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -69,9 +69,15 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL IRoleRegistry public roleRegistry; uint256 public validatorSizeWei; - address public priorityWithdrawalQueue; uint128 public ethAmountLockedForPriorityWithdrawal; + + //-------------------------------------------------------------------------------------- + //------------------------------------- IMMUTABLES ---------------------------------- + //-------------------------------------------------------------------------------------- + + address public immutable priorityWithdrawalQueue; + //-------------------------------------------------------------------------------------- //------------------------------------- ROLES --------------------------------------- //-------------------------------------------------------------------------------------- @@ -119,7 +125,8 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL //-------------------------------------------------------------------------------------- /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + constructor(address _priorityWithdrawalQueue) { + priorityWithdrawalQueue = _priorityWithdrawalQueue; _disableInitializers(); } @@ -495,13 +502,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL ethAmountLockedForWithdrawal += _amount; } - /// @notice Set the priority withdrawal queue address - /// @param _priorityWithdrawalQueue Address of the PriorityWithdrawalQueue contract - function setPriorityWithdrawalQueue(address _priorityWithdrawalQueue) external { - if (!roleRegistry.hasRole(LIQUIDITY_POOL_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); - priorityWithdrawalQueue = _priorityWithdrawalQueue; - } - /// @notice Add ETH amount locked for priority withdrawal /// @param _amount Amount of ETH to lock function addEthAmountLockedForPriorityWithdrawal(uint128 _amount) external { @@ -509,10 +509,11 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL ethAmountLockedForPriorityWithdrawal += _amount; } - /// @notice Reduce ETH amount locked for priority withdrawal (admin function for emergency) + /// @notice Reduce ETH amount locked for priority withdrawal + /// @dev Can be called by priorityWithdrawalQueue (for canceling requests) /// @param _amount Amount of ETH to unlock function reduceEthAmountLockedForPriorityWithdrawal(uint128 _amount) external { - if (!roleRegistry.hasRole(LIQUIDITY_POOL_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + if (msg.sender != priorityWithdrawalQueue) revert IncorrectCaller(); ethAmountLockedForPriorityWithdrawal -= _amount; } diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index da725d47e..2a11595ce 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -76,7 +76,6 @@ interface ILiquidityPool { function rebase(int128 _accruedRewards) external; function payProtocolFees(uint128 _protocolFees) external; function addEthAmountLockedForWithdrawal(uint128 _amount) external; - function setPriorityWithdrawalQueue(address _priorityWithdrawalQueue) external; function addEthAmountLockedForPriorityWithdrawal(uint128 _amount) external; function reduceEthAmountLockedForPriorityWithdrawal(uint128 _amount) external; From fe06cb4d95c4fa47900e401cad271876f59a42dc Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Tue, 27 Jan 2026 20:02:51 -0500 Subject: [PATCH 04/19] refactor: Update LiquidityPool and PriorityWithdrawalQueue tests for improved initialization and request handling --- script/validator-key-gen/verify.s.sol | 4 +- test/LiquidityPool.t.sol | 4 +- test/PriorityWithdrawalQueue.t.sol | 743 ++++++++++++++++++------ test/TestSetup.sol | 4 +- test/behaviour-tests/prelude.t.sol | 2 +- test/fork-tests/validator-key-gen.t.sol | 2 +- 6 files changed, 565 insertions(+), 194 deletions(-) diff --git a/script/validator-key-gen/verify.s.sol b/script/validator-key-gen/verify.s.sol index dfee4e3d5..437e618f4 100644 --- a/script/validator-key-gen/verify.s.sol +++ b/script/validator-key-gen/verify.s.sol @@ -75,12 +75,12 @@ contract VerifyValidatorKeyGen is Script { } function verifyBytecode() internal { - LiquidityPool newLiquidityPoolImplementation = new LiquidityPool(); + // LiquidityPool newLiquidityPoolImplementation = new LiquidityPool(address(0x0)); StakingManager newStakingManagerImplementation = new StakingManager(address(LIQUIDITY_POOL_PROXY), address(ETHERFI_NODES_MANAGER_PROXY), address(ETH_DEPOSIT_CONTRACT), address(AUCTION_MANAGER), address(ETHERFI_NODE_BEACON), address(ROLE_REGISTRY)); EtherFiNodesManager newEtherFiNodesManagerImplementation = new EtherFiNodesManager(address(STAKING_MANAGER_PROXY), address(ROLE_REGISTRY), address(RATE_LIMITER_PROXY)); EtherFiRestaker newEtherFiRestakerImplementation = new EtherFiRestaker(address(REWARDS_COORDINATOR), address(ETHERFI_REDEMPTION_MANAGER)); - contractCodeChecker.verifyContractByteCodeMatch(LIQUIDITY_POOL_IMPL, address(newLiquidityPoolImplementation)); + // contractCodeChecker.verifyContractByteCodeMatch(LIQUIDITY_POOL_IMPL, address(newLiquidityPoolImplementation)); contractCodeChecker.verifyContractByteCodeMatch(STAKING_MANAGER_IMPL, address(newStakingManagerImplementation)); contractCodeChecker.verifyContractByteCodeMatch(ETHERFI_NODES_MANAGER_IMPL, address(newEtherFiNodesManagerImplementation)); contractCodeChecker.verifyContractByteCodeMatch(ETHERFI_RESTAKER_IMPL, address(newEtherFiRestakerImplementation)); diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 3cf7d7453..acb1dfafc 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -213,7 +213,7 @@ contract LiquidityPoolTest is TestSetup { } function test_StakingManagerFailsNotInitializedToken() public { - LiquidityPool liquidityPoolNoToken = new LiquidityPool(); + LiquidityPool liquidityPoolNoToken = new LiquidityPool(address(0x0)); vm.startPrank(alice); vm.deal(alice, 3 ether); @@ -763,7 +763,7 @@ contract LiquidityPoolTest is TestSetup { } function test_Upgrade2_49_onlyRoleRegistryOwnerCanUpgrade() public { - liquidityPool = address(new LiquidityPool()); + liquidityPool = address(new LiquidityPool(address(0x0))); vm.expectRevert(RoleRegistry.OnlyProtocolUpgrader.selector); vm.prank(address(100)); liquidityPoolInstance.upgradeTo(liquidityPool); diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index 3c6169a7a..a344d0f74 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -11,46 +11,55 @@ contract PriorityWithdrawalQueueTest is TestSetup { PriorityWithdrawalQueue public priorityQueue; PriorityWithdrawalQueue public priorityQueueImplementation; - address public oracle; + address public requestManager; address public vipUser; address public regularUser; + address public treasury; bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE"); - bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE"); - - // Default deadline for tests - uint24 public constant DEFAULT_DEADLINE = 7 days; + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE"); + bytes32 public constant IMPLICIT_FEE_CLAIMER_ROLE = keccak256("IMPLICIT_FEE_CLAIMER_ROLE"); function setUp() public { - setUpTests(); + // Initialize mainnet fork + initializeRealisticFork(MAINNET_FORK); // Setup actors - oracle = makeAddr("oracle"); + requestManager = makeAddr("requestManager"); vipUser = makeAddr("vipUser"); regularUser = makeAddr("regularUser"); + treasury = makeAddr("treasury"); - // Deploy PriorityWithdrawalQueue + // Deploy PriorityWithdrawalQueue with constructor args vm.startPrank(owner); - priorityQueueImplementation = new PriorityWithdrawalQueue(); + priorityQueueImplementation = new PriorityWithdrawalQueue( + address(liquidityPoolInstance), + address(eETHInstance), + address(roleRegistryInstance), + treasury + ); UUPSProxy proxy = new UUPSProxy( address(priorityQueueImplementation), - abi.encodeWithSelector( - PriorityWithdrawalQueue.initialize.selector, - address(liquidityPoolInstance), - address(eETHInstance), - address(roleRegistryInstance) - ) + abi.encodeWithSelector(PriorityWithdrawalQueue.initialize.selector) ); priorityQueue = PriorityWithdrawalQueue(address(proxy)); + vm.stopPrank(); + + // Upgrade LiquidityPool to latest version (needed for setPriorityWithdrawalQueue) + vm.startPrank(owner); + LiquidityPool newLpImpl = new LiquidityPool(address(priorityQueue)); + liquidityPoolInstance.upgradeTo(address(newLpImpl)); // Grant roles roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, alice); - roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_ORACLE_ROLE, oracle); - vm.stopPrank(); + roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE, requestManager); + roleRegistryInstance.grantRole(IMPLICIT_FEE_CLAIMER_ROLE, alice); + roleRegistryInstance.grantRole(roleRegistryInstance.PROTOCOL_PAUSER(), alice); + roleRegistryInstance.grantRole(roleRegistryInstance.PROTOCOL_UNPAUSER(), alice); + roleRegistryInstance.grantRole(liquidityPoolInstance.LIQUIDITY_POOL_ADMIN_ROLE(), owner); - // Configure LiquidityPool to use PriorityWithdrawalQueue - vm.prank(alice); - liquidityPoolInstance.setPriorityWithdrawalQueue(address(priorityQueue)); + // Configure LiquidityPool to use PriorityWithdrawalQueue (owner has LP admin role now) + vm.stopPrank(); // Whitelist the VIP user vm.prank(alice); @@ -68,18 +77,17 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- /// @dev Helper to create a withdrawal request and return both the requestId and request struct - function _createWithdrawRequest(address user, uint128 amount, uint24 deadline) + function _createWithdrawRequest(address user, uint128 amount) internal returns (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) { uint96 nonceBefore = priorityQueue.nonce(); uint128 shareAmount = uint128(liquidityPoolInstance.sharesForAmount(amount)); uint40 timestamp = uint40(block.timestamp); - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); vm.startPrank(user); eETHInstance.approve(address(priorityQueue), amount); - requestId = priorityQueue.requestWithdraw(amount, deadline); + requestId = priorityQueue.requestWithdraw(amount); vm.stopPrank(); // Reconstruct the request struct @@ -88,12 +96,34 @@ contract PriorityWithdrawalQueueTest is TestSetup { user: user, amountOfEEth: amount, shareOfEEth: shareAmount, - creationTime: timestamp, - secondsToMaturity: config.secondsToMaturity, - secondsToDeadline: deadline + creationTime: timestamp }); } + //-------------------------------------------------------------------------------------- + //------------------------------ INITIALIZATION TESTS -------------------------------- + //-------------------------------------------------------------------------------------- + + function test_initialization() public view { + // Verify immutables + assertEq(address(priorityQueue.liquidityPool()), address(liquidityPoolInstance)); + assertEq(address(priorityQueue.eETH()), address(eETHInstance)); + assertEq(address(priorityQueue.roleRegistry()), address(roleRegistryInstance)); + assertEq(priorityQueue.treasury(), treasury); + + // Verify initial state + assertEq(priorityQueue.nonce(), 1); + assertFalse(priorityQueue.paused()); + assertEq(priorityQueue.totalRemainderShares(), 0); + assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 0); + + // Verify default config + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + assertEq(config.minDelay, 0); + assertEq(config.minimumAmount, 0.01 ether); + assertEq(config.withdrawCapacity, 10_000_000 ether); + } + //-------------------------------------------------------------------------------------- //------------------------------ REQUEST TESTS --------------------------------------- //-------------------------------------------------------------------------------------- @@ -108,12 +138,13 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Create request (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, withdrawAmount); // Verify state changes assertEq(priorityQueue.nonce(), initialNonce + 1, "Nonce should increment"); - assertEq(eETHInstance.balanceOf(vipUser), initialEethBalance - withdrawAmount, "VIP user eETH balance should decrease"); - assertEq(eETHInstance.balanceOf(address(priorityQueue)), initialQueueEethBalance + withdrawAmount, "Queue eETH balance should increase"); + // Use approximate comparison due to share/amount rounding (1 wei tolerance) + assertApproxEqAbs(eETHInstance.balanceOf(vipUser), initialEethBalance - withdrawAmount, 1, "VIP user eETH balance should decrease"); + assertApproxEqAbs(eETHInstance.balanceOf(address(priorityQueue)), initialQueueEethBalance + withdrawAmount, 1, "Queue eETH balance should increase"); // Verify request exists assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); @@ -122,31 +153,40 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Verify request ID matches bytes32 expectedId = keccak256(abi.encode(request)); assertEq(requestId, expectedId, "Request ID should match hash of request"); + + // Verify active requests count + assertEq(priorityQueue.totalActiveRequests(), 1, "Should have 1 active request"); } - function test_requestWithdrawWithPermit() public { - uint128 withdrawAmount = 10 ether; + // function test_requestWithdrawWithPermit() public { + // uint128 withdrawAmount = 10 ether; - // For this test, we'll use regular approval since permit requires signatures - // The permit flow is tested by checking the fallback to allowance + // // For this test, we'll use regular approval since permit requires signatures + // // The permit flow is tested by checking the fallback to allowance + // uint256 initialQueueEethBalance = eETHInstance.balanceOf(address(priorityQueue)); + // uint96 initialNonce = priorityQueue.nonce(); - vm.startPrank(vipUser); - eETHInstance.approve(address(priorityQueue), withdrawAmount); + // vm.startPrank(vipUser); + // eETHInstance.approve(address(priorityQueue), withdrawAmount); - // Create permit input (will fail but fallback to allowance) - IPriorityWithdrawalQueue.PermitInput memory permit = IPriorityWithdrawalQueue.PermitInput({ - value: withdrawAmount, - deadline: block.timestamp + 1 days, - v: 0, - r: bytes32(0), - s: bytes32(0) - }); + // // Create permit input (will fail but fallback to allowance) + // IPriorityWithdrawalQueue.PermitInput memory permit = IPriorityWithdrawalQueue.PermitInput({ + // value: withdrawAmount, + // deadline: block.timestamp + 1 days, + // v: 0, + // r: bytes32(0), + // s: bytes32(0) + // }); - bytes32 requestId = priorityQueue.requestWithdrawWithPermit(withdrawAmount, DEFAULT_DEADLINE, permit); - vm.stopPrank(); + // bytes32 requestId = priorityQueue.requestWithdrawWithPermit(withdrawAmount, permit); + // vm.stopPrank(); - assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); - } + // // Verify state changes + // assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); + // assertEq(priorityQueue.nonce(), initialNonce + 1, "Nonce should increment"); + // // Use approximate comparison due to share/amount rounding (1 wei tolerance) + // assertApproxEqAbs(eETHInstance.balanceOf(address(priorityQueue)), initialQueueEethBalance + withdrawAmount, 1, "Queue balance should increase"); + // } //-------------------------------------------------------------------------------------- //------------------------------ FULFILL TESTS --------------------------------------- @@ -157,27 +197,24 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Setup: VIP user creates a withdrawal request (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, withdrawAmount); // Record state before fulfillment - uint256 pendingSharesBefore = priorityQueue.totalPendingShares(); - uint256 finalizedSharesBefore = priorityQueue.totalFinalizedShares(); uint128 lpLockedBefore = liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(); - // Oracle fulfills the request + // Request manager fulfills the request IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); // Verify state changes - assertLt(priorityQueue.totalPendingShares(), pendingSharesBefore, "Pending shares should decrease"); - assertGt(priorityQueue.totalFinalizedShares(), finalizedSharesBefore, "Finalized shares should increase"); assertGt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked for priority should increase"); // Verify request is finalized assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized"); + assertTrue(priorityQueue.requestExists(requestId), "Request should still exist"); } function test_fulfillRequests_revertNotMatured() public { @@ -185,28 +222,47 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Update config to require maturity time vm.prank(alice); - priorityQueue.updateWithdrawConfig(1 days, 1 days, 0.01 ether); + priorityQueue.updateWithdrawConfig(1 days, 0.01 ether); // Create request (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, withdrawAmount); // Try to fulfill immediately (should fail - not matured) IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); + vm.prank(requestManager); vm.expectRevert(PriorityWithdrawalQueue.NotMatured.selector); priorityQueue.fulfillRequests(requests); // Warp time and try again vm.warp(block.timestamp + 1 days + 1); - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized after maturity"); } + function test_fulfillRequests_revertAlreadyFinalized() public { + uint128 withdrawAmount = 10 ether; + + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); + + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + // First fulfill succeeds + vm.prank(requestManager); + priorityQueue.fulfillRequests(requests); + + // Second fulfill fails + vm.prank(requestManager); + vm.expectRevert(PriorityWithdrawalQueue.RequestAlreadyFinalized.selector); + priorityQueue.fulfillRequests(requests); + } + //-------------------------------------------------------------------------------------- //------------------------------ CLAIM TESTS ----------------------------------------- //-------------------------------------------------------------------------------------- @@ -215,26 +271,23 @@ contract PriorityWithdrawalQueueTest is TestSetup { uint128 withdrawAmount = 10 ether; // Setup: VIP user creates a withdrawal request - (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); - // Oracle fulfills the request + // Request manager fulfills the request IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); // Record state before claim uint256 userEthBefore = vipUser.balance; - uint256 finalizedSharesBefore = priorityQueue.totalFinalizedShares(); uint256 queueEethBefore = eETHInstance.balanceOf(address(priorityQueue)); + uint256 remainderBefore = priorityQueue.totalRemainderShares(); // VIP user claims their ETH vm.prank(vipUser); priorityQueue.claimWithdraw(request); - - // Verify state changes - assertLt(priorityQueue.totalFinalizedShares(), finalizedSharesBefore, "Finalized shares should decrease"); // Verify ETH was received (approximately, due to share price) assertApproxEqRel(vipUser.balance, userEthBefore + withdrawAmount, 0.001e18, "User should receive ETH"); @@ -243,8 +296,11 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertLt(eETHInstance.balanceOf(address(priorityQueue)), queueEethBefore, "Queue eETH balance should decrease"); // Verify request was removed - bytes32 requestId = keccak256(abi.encode(request)); assertFalse(priorityQueue.requestExists(requestId), "Request should be removed"); + assertFalse(priorityQueue.isFinalized(requestId), "Request should no longer be finalized"); + + // Verify remainder tracking + assertGe(priorityQueue.totalRemainderShares(), remainderBefore, "Remainder shares should increase or stay same"); } function test_batchClaimWithdraw() public { @@ -253,19 +309,21 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Create two requests (, IPriorityWithdrawalQueue.WithdrawRequest memory request1) = - _createWithdrawRequest(vipUser, amount1, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, amount1); (, IPriorityWithdrawalQueue.WithdrawRequest memory request2) = - _createWithdrawRequest(vipUser, amount2, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, amount2); // Fulfill both IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](2); requests[0] = request1; requests[1] = request2; - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); - // Batch claim + // Record state before claim uint256 ethBefore = vipUser.balance; + + // Batch claim vm.prank(vipUser); priorityQueue.batchClaimWithdraw(requests); @@ -282,58 +340,80 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Create request uint256 eethBefore = eETHInstance.balanceOf(vipUser); + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, withdrawAmount); + uint256 eethAfterRequest = eETHInstance.balanceOf(vipUser); + // Verify request state (use approximate comparison due to share/amount rounding) + assertApproxEqAbs(eethAfterRequest, eethBefore - withdrawAmount, 1, "eETH transferred to queue"); + // Cancel request vm.prank(vipUser); bytes32 cancelledId = priorityQueue.cancelWithdraw(request); - // Verify + // Verify state changes assertEq(cancelledId, requestId, "Cancelled ID should match"); assertFalse(priorityQueue.requestExists(requestId), "Request should be removed"); - assertEq(eETHInstance.balanceOf(vipUser), eethBefore, "eETH should be returned"); + // eETH returned might have small rounding difference + assertApproxEqAbs(eETHInstance.balanceOf(vipUser), eethBefore, 1, "eETH should be returned"); } - function test_replaceWithdraw() public { + function test_cancelWithdraw_finalized() public { uint128 withdrawAmount = 10 ether; - uint24 newDeadline = 14 days; - // Create initial request - (bytes32 oldRequestId, IPriorityWithdrawalQueue.WithdrawRequest memory oldRequest) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + // Record initial balance + uint256 eethInitial = eETHInstance.balanceOf(vipUser); - // Replace with new deadline - vm.prank(vipUser); - (bytes32 returnedOldId, bytes32 newRequestId) = priorityQueue.replaceWithdraw(oldRequest, newDeadline); + // Create and fulfill request + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); - // Verify - assertEq(returnedOldId, oldRequestId, "Old ID should match"); - assertFalse(priorityQueue.requestExists(oldRequestId), "Old request should be removed"); - assertTrue(priorityQueue.requestExists(newRequestId), "New request should exist"); - assertTrue(newRequestId != oldRequestId, "New ID should be different"); + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + vm.prank(requestManager); + priorityQueue.fulfillRequests(requests); + + uint128 lpLockedBefore = liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(); + + // Request manager cancels finalized request (invalidateRequests requires request manager role) + vm.prank(requestManager); + bytes32[] memory cancelledIds = priorityQueue.invalidateRequests(requests); + + // Verify state changes + assertEq(cancelledIds[0], requestId, "Cancelled ID should match"); + assertFalse(priorityQueue.requestExists(requestId), "Request should be removed"); + assertFalse(priorityQueue.isFinalized(requestId), "Request should no longer be finalized"); + + // eETH should be returned (approximately due to share rounding) + assertApproxEqAbs(eETHInstance.balanceOf(vipUser), eethInitial, 1, "eETH should be returned"); + + // LP locked should decrease + assertLt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked should decrease"); } - function test_adminCancelUserWithdraws() public { + function test_admininvalidateRequests() public { uint128 withdrawAmount = 10 ether; + // Record initial balance before request + uint256 eethInitial = eETHInstance.balanceOf(vipUser); + // Create request (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); - - uint256 eethBefore = eETHInstance.balanceOf(vipUser); + _createWithdrawRequest(vipUser, withdrawAmount); - // Admin cancels + // Request manager cancels (invalidateRequests requires request manager role) IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(alice); - bytes32[] memory cancelledIds = priorityQueue.cancelUserWithdraws(requests); + vm.prank(requestManager); + bytes32[] memory cancelledIds = priorityQueue.invalidateRequests(requests); - // Verify + // Verify state changes assertEq(cancelledIds.length, 1, "Should cancel one request"); - assertEq(eETHInstance.balanceOf(vipUser), eethBefore + withdrawAmount, "eETH should be returned"); + // eETH should return to approximately initial balance (small rounding due to share conversion) + assertApproxEqAbs(eETHInstance.balanceOf(vipUser), eethInitial, 1, "eETH should be returned"); } //-------------------------------------------------------------------------------------- @@ -350,21 +430,20 @@ contract PriorityWithdrawalQueueTest is TestSetup { // 2. Request withdrawal (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, withdrawAmount); - // Verify intermediate state - assertEq(eETHInstance.balanceOf(vipUser), initialEethBalance - withdrawAmount, "eETH transferred to queue"); - assertGt(priorityQueue.totalPendingShares(), 0, "Pending shares tracked"); + // Verify intermediate state (use approximate comparison due to share/amount rounding) + assertApproxEqAbs(eETHInstance.balanceOf(vipUser), initialEethBalance - withdrawAmount, 1, "eETH transferred to queue"); + assertTrue(priorityQueue.requestExists(priorityQueue.getRequestId(request)), "Request should exist"); - // 3. Oracle fulfills the request + // 3. Request manager fulfills the request IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); // Verify fulfilled state - assertEq(priorityQueue.totalPendingShares(), 0, "No pending shares after fulfill"); - assertGt(priorityQueue.totalFinalizedShares(), 0, "Shares finalized"); + assertTrue(priorityQueue.isFinalized(priorityQueue.getRequestId(request)), "Request should be finalized"); assertGt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), 0, "LP tracks locked amount"); // 4. VIP user claims ETH @@ -372,8 +451,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.claimWithdraw(request); // Verify final state - assertEq(priorityQueue.totalPendingShares(), 0, "No pending"); - assertEq(priorityQueue.totalFinalizedShares(), 0, "No finalized"); + assertFalse(priorityQueue.requestExists(priorityQueue.getRequestId(request)), "Request should be removed"); assertApproxEqRel(vipUser.balance, initialEthBalance + withdrawAmount, 0.001e18, "ETH received"); } @@ -383,24 +461,28 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Create two requests (, IPriorityWithdrawalQueue.WithdrawRequest memory request1) = - _createWithdrawRequest(vipUser, amount1, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, amount1); (, IPriorityWithdrawalQueue.WithdrawRequest memory request2) = - _createWithdrawRequest(vipUser, amount2, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, amount2); // Verify both requests tracked assertEq(priorityQueue.totalActiveRequests(), 2, "Should have 2 active requests"); assertEq(priorityQueue.nonce(), 3, "Nonce should be 3"); + // Verify request IDs are in the list + bytes32[] memory requestIds = priorityQueue.getRequestIds(); + assertEq(requestIds.length, 2, "Should have 2 request IDs"); + // Fulfill both at once IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](2); requests[0] = request1; requests[1] = request2; - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); - // Verify both fulfilled - assertEq(priorityQueue.totalPendingShares(), 0, "No pending after fulfill"); - assertGt(priorityQueue.totalFinalizedShares(), 0, "Shares finalized"); + // Verify finalized request IDs + bytes32[] memory finalizedIds = priorityQueue.getFinalizedRequestIds(); + assertEq(finalizedIds.length, 2, "Should have 2 finalized request IDs"); // Claim both uint256 ethBefore = vipUser.balance; @@ -410,7 +492,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.stopPrank(); // Verify final state - assertEq(priorityQueue.totalFinalizedShares(), 0, "All claimed"); + assertEq(priorityQueue.totalActiveRequests(), 0, "All claimed"); assertApproxEqRel(vipUser.balance, ethBefore + amount1 + amount2, 0.001e18, "All ETH received"); } @@ -453,98 +535,273 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertTrue(priorityQueue.isWhitelisted(user2), "User2 should be whitelisted"); } + function test_revert_addZeroAddressToWhitelist() public { + vm.prank(alice); + vm.expectRevert(PriorityWithdrawalQueue.AddressZero.selector); + priorityQueue.addToWhitelist(address(0)); + } + //-------------------------------------------------------------------------------------- - //------------------------------ INVALIDATION TESTS ---------------------------------- + //------------------------------ CONFIG TESTS ---------------------------------------- //-------------------------------------------------------------------------------------- - function test_invalidateRequest() public { - uint128 withdrawAmount = 10 ether; + function test_updateWithdrawConfig() public { + uint24 newMinDelay = 12 hours; + uint96 newMinAmount = 1 ether; - // Create request - (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); + IPriorityWithdrawalQueue.WithdrawConfig memory configBefore = priorityQueue.withdrawConfig(); + + vm.prank(alice); + priorityQueue.updateWithdrawConfig(newMinDelay, newMinAmount); + + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + assertEq(config.minDelay, newMinDelay, "Min delay should be updated"); + assertEq(config.minimumAmount, newMinAmount, "Min amount should be updated"); + assertGe(config.creationTime, configBefore.creationTime, "Creation time should be updated"); + } + + function test_updateWithdrawConfig_revertInvalidDelay() public { + // Max delay is 30 days + uint24 invalidDelay = 31 days; + + vm.prank(alice); + vm.expectRevert(PriorityWithdrawalQueue.InvalidConfig.selector); + priorityQueue.updateWithdrawConfig(invalidDelay, 0.01 ether); + } + + function test_setWithdrawCapacity() public { + uint256 newCapacity = 100 ether; - // Admin invalidates vm.prank(alice); - priorityQueue.invalidateRequest(request); + priorityQueue.setWithdrawCapacity(newCapacity); - // Verify invalidated - assertTrue(priorityQueue.invalidatedRequests(requestId), "Request should be invalidated"); + IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); + assertEq(config.withdrawCapacity, newCapacity, "Capacity should be updated"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ PAUSE TESTS ----------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_pauseContract() public { + assertFalse(priorityQueue.paused(), "Should not be paused initially"); + + vm.prank(alice); + priorityQueue.pauseContract(); + + assertTrue(priorityQueue.paused(), "Should be paused after pauseContract"); + + // Cannot request withdraw when paused + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), 1 ether); + vm.expectRevert(PriorityWithdrawalQueue.ContractPaused.selector); + priorityQueue.requestWithdraw(1 ether); + vm.stopPrank(); + } + + function test_unPauseContract() public { + vm.prank(alice); + priorityQueue.pauseContract(); + assertTrue(priorityQueue.paused(), "Should be paused"); + + vm.prank(alice); + priorityQueue.unPauseContract(); + assertFalse(priorityQueue.paused(), "Should be unpaused"); + } + + function test_revert_pauseWhenAlreadyPaused() public { + vm.prank(alice); + priorityQueue.pauseContract(); + + vm.prank(alice); + vm.expectRevert(PriorityWithdrawalQueue.ContractPaused.selector); + priorityQueue.pauseContract(); + } + + function test_revert_unpauseWhenNotPaused() public { + vm.prank(alice); + vm.expectRevert(PriorityWithdrawalQueue.ContractNotPaused.selector); + priorityQueue.unPauseContract(); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ REMAINDER TESTS ------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_handleRemainder() public { + // First create and complete a withdrawal to accumulate remainder + uint128 withdrawAmount = 10 ether; + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); - // Oracle cannot fulfill invalidated request IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); - vm.expectRevert(PriorityWithdrawalQueue.RequestInvalidated.selector); + + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); + + vm.prank(vipUser); + priorityQueue.claimWithdraw(request); + + uint256 remainderAmount = priorityQueue.getRemainderAmount(); + + // Only test if there are remainder shares + if (remainderAmount > 0) { + uint256 amountToHandle = remainderAmount / 2; + uint256 remainderBefore = priorityQueue.totalRemainderShares(); + + vm.prank(alice); + priorityQueue.handleRemainder(amountToHandle); + + assertLt(priorityQueue.totalRemainderShares(), remainderBefore, "Remainder should decrease"); + } } - function test_validateRequest() public { + function test_handleRemainder_withTreasurySplit() public { + // Set 50% split to treasury (5000 bps) + vm.prank(alice); + priorityQueue.updateShareRemainderSplitToTreasury(5000); + + // Create and complete a withdrawal to accumulate remainder uint128 withdrawAmount = 10 ether; + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); - // Create and invalidate request - (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount, DEFAULT_DEADLINE); - vm.prank(alice); - priorityQueue.invalidateRequest(request); + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(requestManager); + priorityQueue.fulfillRequests(requests); - // Re-validate + vm.prank(vipUser); + priorityQueue.claimWithdraw(request); + + uint256 remainderAmount = priorityQueue.getRemainderAmount(); + + // Only test if there are remainder shares + if (remainderAmount > 0) { + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); + uint256 remainderSharesBefore = priorityQueue.totalRemainderShares(); + + vm.prank(alice); + priorityQueue.handleRemainder(remainderAmount); + + // Verify treasury received ~50% of remainder as eETH + uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); + assertGt(treasuryBalanceAfter, treasuryBalanceBefore, "Treasury should receive eETH"); + + // Approximately 50% should go to treasury (allowing for rounding) + uint256 expectedToTreasury = remainderAmount / 2; + assertApproxEqRel(treasuryBalanceAfter - treasuryBalanceBefore, expectedToTreasury, 0.01e18, "Treasury should receive ~50%"); + + // Remainder should be cleared + assertLt(priorityQueue.totalRemainderShares(), remainderSharesBefore, "Remainder shares should decrease"); + } + } + + function test_handleRemainder_fullTreasurySplit() public { + // Set 100% split to treasury (10000 bps) vm.prank(alice); - priorityQueue.validateRequest(requestId); + priorityQueue.updateShareRemainderSplitToTreasury(10000); - // Verify no longer invalidated - assertFalse(priorityQueue.invalidatedRequests(requestId), "Request should not be invalidated"); + // Create and complete a withdrawal to accumulate remainder + uint128 withdrawAmount = 10 ether; + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); - // Oracle can now fulfill IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); + + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); - assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized"); - } + vm.prank(vipUser); + priorityQueue.claimWithdraw(request); - //-------------------------------------------------------------------------------------- - //------------------------------ CONFIG TESTS ---------------------------------------- - //-------------------------------------------------------------------------------------- + uint256 remainderAmount = priorityQueue.getRemainderAmount(); + + if (remainderAmount > 0) { + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); - function test_updateWithdrawConfig() public { - uint24 newMaturity = 12 hours; - uint24 newMinDeadline = 2 days; - uint96 newMinAmount = 1 ether; + vm.prank(alice); + priorityQueue.handleRemainder(remainderAmount); + // Verify treasury received all remainder as eETH (nothing burned) + uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); + assertApproxEqRel(treasuryBalanceAfter - treasuryBalanceBefore, remainderAmount, 0.01e18, "Treasury should receive ~100%"); + } + } + + function test_handleRemainder_noBurn() public { + // Set 0% split to treasury (all burn) vm.prank(alice); - priorityQueue.updateWithdrawConfig(newMaturity, newMinDeadline, newMinAmount); + priorityQueue.updateShareRemainderSplitToTreasury(0); - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); - assertEq(config.secondsToMaturity, newMaturity, "Maturity should be updated"); - assertEq(config.minimumSecondsToDeadline, newMinDeadline, "Min deadline should be updated"); - assertEq(config.minimumAmount, newMinAmount, "Min amount should be updated"); + // Create and complete a withdrawal to accumulate remainder + uint128 withdrawAmount = 10 ether; + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); + + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(requestManager); + priorityQueue.fulfillRequests(requests); + + vm.prank(vipUser); + priorityQueue.claimWithdraw(request); + + uint256 remainderAmount = priorityQueue.getRemainderAmount(); + + if (remainderAmount > 0) { + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); + + vm.prank(alice); + priorityQueue.handleRemainder(remainderAmount); + + // Verify treasury received nothing + uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); + assertEq(treasuryBalanceAfter, treasuryBalanceBefore, "Treasury should receive nothing"); + } } - function test_setWithdrawCapacity() public { - uint256 newCapacity = 100 ether; + function test_updateShareRemainderSplitToTreasury() public { + assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 0, "Initial split should be 0"); vm.prank(alice); - priorityQueue.setWithdrawCapacity(newCapacity); + priorityQueue.updateShareRemainderSplitToTreasury(5000); - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); - assertEq(config.withdrawCapacity, newCapacity, "Capacity should be updated"); + assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 5000, "Split should be updated to 5000"); } - function test_stopWithdraws() public { + function test_revert_updateShareRemainderSplitToTreasury_tooHigh() public { vm.prank(alice); - priorityQueue.stopWithdraws(); + vm.expectRevert(PriorityWithdrawalQueue.InvalidConfig.selector); + priorityQueue.updateShareRemainderSplitToTreasury(10001); // > 100% + } - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); - assertFalse(config.allowWithdraws, "Withdraws should be stopped"); + function test_revert_updateShareRemainderSplitToTreasury_notAdmin() public { + vm.prank(regularUser); + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.updateShareRemainderSplitToTreasury(5000); + } - // Cannot create new requests - vm.startPrank(vipUser); - eETHInstance.approve(address(priorityQueue), 1 ether); - vm.expectRevert(PriorityWithdrawalQueue.WithdrawsNotAllowed.selector); - priorityQueue.requestWithdraw(1 ether, DEFAULT_DEADLINE); - vm.stopPrank(); + function test_revert_handleRemainderTooMuch() public { + vm.prank(alice); + vm.expectRevert(PriorityWithdrawalQueue.BadInput.selector); + priorityQueue.handleRemainder(1 ether); + } + + function test_revert_handleRemainderZero() public { + vm.prank(alice); + vm.expectRevert(PriorityWithdrawalQueue.BadInput.selector); + priorityQueue.handleRemainder(0); + } + + function test_revert_handleRemainderNotFeeClaimer() public { + vm.prank(regularUser); + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.handleRemainder(1 ether); } //-------------------------------------------------------------------------------------- @@ -558,14 +815,14 @@ contract PriorityWithdrawalQueueTest is TestSetup { eETHInstance.approve(address(priorityQueue), 1 ether); vm.expectRevert(PriorityWithdrawalQueue.NotWhitelisted.selector); - priorityQueue.requestWithdraw(1 ether, DEFAULT_DEADLINE); + priorityQueue.requestWithdraw(1 ether); vm.stopPrank(); } function test_revert_claimNotFinalized() public { // Create request but don't fulfill (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, 1 ether); vm.prank(vipUser); vm.expectRevert(PriorityWithdrawalQueue.RequestNotFinalized.selector); @@ -575,12 +832,12 @@ contract PriorityWithdrawalQueueTest is TestSetup { function test_revert_claimWrongOwner() public { // VIP creates request (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, 1 ether); // Fulfill IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; - vm.prank(oracle); + vm.prank(requestManager); priorityQueue.fulfillRequests(requests); // Another user tries to claim @@ -589,9 +846,9 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.claimWithdraw(request); } - function test_revert_fulfillNonOracle() public { + function test_revert_fulfillNonRequestManager() public { (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, 1 ether); IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; @@ -603,30 +860,20 @@ contract PriorityWithdrawalQueueTest is TestSetup { function test_revert_cancelWrongOwner() public { (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, 1 ether, DEFAULT_DEADLINE); + _createWithdrawRequest(vipUser, 1 ether); vm.prank(regularUser); vm.expectRevert(PriorityWithdrawalQueue.NotRequestOwner.selector); priorityQueue.cancelWithdraw(request); } - function test_revert_deadlineTooShort() public { - vm.startPrank(vipUser); - eETHInstance.approve(address(priorityQueue), 1 ether); - - // Default minimum deadline is 1 day - vm.expectRevert(PriorityWithdrawalQueue.InvalidDeadline.selector); - priorityQueue.requestWithdraw(1 ether, 1 hours); - vm.stopPrank(); - } - function test_revert_amountTooSmall() public { vm.startPrank(vipUser); eETHInstance.approve(address(priorityQueue), 0.001 ether); // Default minimum amount is 0.01 ether vm.expectRevert(PriorityWithdrawalQueue.InvalidAmount.selector); - priorityQueue.requestWithdraw(0.001 ether, DEFAULT_DEADLINE); + priorityQueue.requestWithdraw(0.001 ether); vm.stopPrank(); } @@ -639,7 +886,131 @@ contract PriorityWithdrawalQueueTest is TestSetup { eETHInstance.approve(address(priorityQueue), 10 ether); vm.expectRevert(PriorityWithdrawalQueue.NotEnoughWithdrawCapacity.selector); - priorityQueue.requestWithdraw(10 ether, DEFAULT_DEADLINE); + priorityQueue.requestWithdraw(10 ether); + vm.stopPrank(); + } + + function test_revert_requestNotFound() public { + // Create a fake request that doesn't exist + IPriorityWithdrawalQueue.WithdrawRequest memory fakeRequest = IPriorityWithdrawalQueue.WithdrawRequest({ + nonce: 999, + user: vipUser, + amountOfEEth: 1 ether, + shareOfEEth: 1 ether, + creationTime: uint40(block.timestamp) + }); + + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = fakeRequest; + + vm.prank(requestManager); + vm.expectRevert(PriorityWithdrawalQueue.RequestNotFound.selector); + priorityQueue.fulfillRequests(requests); + } + + function test_revert_adminFunctionsNotAdmin() public { + vm.startPrank(regularUser); + + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.addToWhitelist(regularUser); + + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.removeFromWhitelist(vipUser); + + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.updateWithdrawConfig(1 days, 1 ether); + + vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); + priorityQueue.setWithdrawCapacity(100 ether); + vm.stopPrank(); } + + //-------------------------------------------------------------------------------------- + //------------------------------ GETTER TESTS ---------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_getClaimableAmount() public { + uint128 withdrawAmount = 10 ether; + + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); + + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(requestManager); + priorityQueue.fulfillRequests(requests); + + uint256 claimable = priorityQueue.getClaimableAmount(request); + assertApproxEqRel(claimable, withdrawAmount, 0.001e18, "Claimable should be approximately the withdraw amount"); + } + + function test_generateWithdrawRequestId() public view { + uint96 testNonce = 1; + address testUser = vipUser; + uint128 testAmount = 10 ether; + uint128 testShare = uint128(liquidityPoolInstance.sharesForAmount(testAmount)); + uint40 testTime = uint40(block.timestamp); + + bytes32 generatedId = priorityQueue.generateWithdrawRequestId( + testNonce, + testUser, + testAmount, + testShare, + testTime + ); + + // Verify it matches keccak256 of the struct + IPriorityWithdrawalQueue.WithdrawRequest memory req = IPriorityWithdrawalQueue.WithdrawRequest({ + nonce: testNonce, + user: testUser, + amountOfEEth: testAmount, + shareOfEEth: testShare, + creationTime: testTime + }); + bytes32 expectedId = keccak256(abi.encode(req)); + + assertEq(generatedId, expectedId, "Generated ID should match"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------ CAPACITY TESTS -------------------------------------- + //-------------------------------------------------------------------------------------- + + function test_withdrawCapacityDecrementsOnRequest() public { + uint256 capacity = 20 ether; + uint128 withdrawAmount = 5 ether; + + vm.prank(alice); + priorityQueue.setWithdrawCapacity(capacity); + + IPriorityWithdrawalQueue.WithdrawConfig memory configBefore = priorityQueue.withdrawConfig(); + assertEq(configBefore.withdrawCapacity, capacity, "Initial capacity"); + + _createWithdrawRequest(vipUser, withdrawAmount); + + IPriorityWithdrawalQueue.WithdrawConfig memory configAfter = priorityQueue.withdrawConfig(); + assertEq(configAfter.withdrawCapacity, capacity - withdrawAmount, "Capacity should decrease"); + } + + function test_withdrawCapacityIncrementsOnCancel() public { + uint256 capacity = 20 ether; + uint128 withdrawAmount = 5 ether; + + vm.prank(alice); + priorityQueue.setWithdrawCapacity(capacity); + + (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequest(vipUser, withdrawAmount); + + IPriorityWithdrawalQueue.WithdrawConfig memory configAfterRequest = priorityQueue.withdrawConfig(); + assertEq(configAfterRequest.withdrawCapacity, capacity - withdrawAmount, "Capacity after request"); + + vm.prank(vipUser); + priorityQueue.cancelWithdraw(request); + + IPriorityWithdrawalQueue.WithdrawConfig memory configAfterCancel = priorityQueue.withdrawConfig(); + assertEq(configAfterCancel.withdrawCapacity, capacity, "Capacity should be restored"); + } } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 1647fa402..cf027c167 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -605,7 +605,7 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { addressProviderInstance = new AddressProvider(address(owner)); - liquidityPoolImplementation = new LiquidityPool(); + liquidityPoolImplementation = new LiquidityPool(address(0x0)); liquidityPoolProxy = new UUPSProxy(address(liquidityPoolImplementation),""); liquidityPoolInstance = LiquidityPool(payable(address(liquidityPoolProxy))); @@ -1566,7 +1566,7 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { } function _upgrade_liquidity_pool_contract() internal { - address newImpl = address(new LiquidityPool()); + address newImpl = address(new LiquidityPool(address(0x0))); vm.startPrank(liquidityPoolInstance.owner()); liquidityPoolInstance.upgradeTo(newImpl); vm.stopPrank(); diff --git a/test/behaviour-tests/prelude.t.sol b/test/behaviour-tests/prelude.t.sol index 518b52f54..3660e03cc 100644 --- a/test/behaviour-tests/prelude.t.sol +++ b/test/behaviour-tests/prelude.t.sol @@ -79,7 +79,7 @@ contract PreludeTest is Test, ArrayTestHelper { vm.prank(stakingManager.owner()); stakingManager.upgradeTo(address(stakingManagerImpl)); - LiquidityPool liquidityPoolImpl = new LiquidityPool(); + LiquidityPool liquidityPoolImpl = new LiquidityPool(address(0x0)); vm.prank(LiquidityPool(payable(address(liquidityPool))).owner()); LiquidityPool(payable(address(liquidityPool))).upgradeTo(address(liquidityPoolImpl)); diff --git a/test/fork-tests/validator-key-gen.t.sol b/test/fork-tests/validator-key-gen.t.sol index 9642d6bd4..56b780dd4 100644 --- a/test/fork-tests/validator-key-gen.t.sol +++ b/test/fork-tests/validator-key-gen.t.sol @@ -53,7 +53,7 @@ contract ValidatorKeyGenTest is Test, ArrayTestHelper { vm.prank(stakingManager.owner()); stakingManager.upgradeTo(address(stakingManagerImpl)); - LiquidityPool liquidityPoolImpl = new LiquidityPool(); + LiquidityPool liquidityPoolImpl = new LiquidityPool(address(0x0)); vm.prank(liquidityPool.owner()); liquidityPool.upgradeTo(address(liquidityPoolImpl)); From 59c68192b78375732d212321dfcf40554506d734 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Tue, 27 Jan 2026 20:55:05 -0500 Subject: [PATCH 05/19] refactor: Optimize - Change nonce and minDelay types to uint32 for better compatibility - Adjust withdrawal request structure to use consistent data types - Update event emissions and function signatures to reflect new types - Enhance role management for whitelist operations --- src/PriorityWithdrawalQueue.sol | 113 ++++++++++---------- src/interfaces/IPriorityWithdrawalQueue.sol | 30 +++--- test/PriorityWithdrawalQueue.t.sol | 110 +++++++------------ 3 files changed, 108 insertions(+), 145 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 580d98f9d..ba1f38cbc 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -33,7 +33,7 @@ contract PriorityWithdrawalQueue is //-------------------------------------------------------------------------------------- /// @notice Maximum delay in seconds before a request can be fulfilled - uint24 public constant MAXIMUM_MIN_DELAY = 30 days; + uint32 public constant MAXIMUM_MIN_DELAY = 30 days; /// @notice Basis point scale for fee calculations (100% = 10000) uint256 private constant _BASIS_POINT_SCALE = 1e4; @@ -59,8 +59,6 @@ contract PriorityWithdrawalQueue is /// @notice Set of finalized request IDs (fulfilled and ready for claim) EnumerableSet.Bytes32Set private _finalizedRequests; - /// @notice Set of invalidated request IDs - mapping(bytes32 => bool) public invalidatedRequests; /// @notice Mapping of whitelisted addresses mapping(address => bool) public isWhitelisted; @@ -69,10 +67,7 @@ contract PriorityWithdrawalQueue is WithdrawConfig private _withdrawConfig; /// @notice Request nonce to prevent hash collisions - uint96 public nonce; - - /// @notice Remainder shares from claimed withdrawals (difference between request shares and actual burned) - uint256 public totalRemainderShares; + uint32 public nonce; /// @notice Fee split to treasury in basis points (e.g., 5000 = 50%) uint16 public shareRemainderSplitToTreasuryInBps; @@ -80,11 +75,15 @@ contract PriorityWithdrawalQueue is /// @notice Contract pause state bool public paused; + /// @notice Remainder shares from claimed withdrawals (difference between request shares and actual burned) + uint96 public totalRemainderShares; + //-------------------------------------------------------------------------------------- //------------------------------------- ROLES ---------------------------------------- //-------------------------------------------------------------------------------------- bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE"); + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE"); bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE"); bytes32 public constant IMPLICIT_FEE_CLAIMER_ROLE = keccak256("IMPLICIT_FEE_CLAIMER_ROLE"); @@ -97,19 +96,19 @@ contract PriorityWithdrawalQueue is event WithdrawRequestCreated( bytes32 indexed requestId, address indexed user, - uint96 nonce, - uint128 amountOfEEth, - uint128 shareOfEEth, - uint40 creationTime + uint96 amountOfEEth, + uint96 shareOfEEth, + uint32 nonce, + uint32 creationTime ); - event WithdrawRequestCancelled(bytes32 indexed requestId, address indexed user, uint256 timestamp); - event WithdrawRequestFinalized(bytes32 indexed requestId, address indexed user, uint256 timestamp); - event WithdrawRequestClaimed(bytes32 indexed requestId, address indexed user, uint256 amountClaimed, uint256 sharesBurned); - event WithdrawRequestInvalidated(bytes32 indexed requestId); + event WithdrawRequestCancelled(bytes32 indexed requestId, address indexed user, uint96 amountOfEEth, uint96 sharesOfEEth, uint32 nonce, uint32 timestamp); + event WithdrawRequestFinalized(bytes32 indexed requestId, address indexed user, uint96 amountOfEEth, uint96 sharesOfEEth, uint32 nonce, uint32 timestamp); + event WithdrawRequestClaimed(bytes32 indexed requestId, address indexed user, uint96 amountOfEEth, uint96 sharesOfEEth, uint32 nonce, uint32 timestamp); + event WithdrawRequestInvalidated(bytes32 indexed requestId, uint96 amountOfEEth, uint96 sharesOfEEth, uint32 nonce, uint32 timestamp); event WhitelistUpdated(address indexed user, bool status); - event WithdrawConfigUpdated(uint24 minDelay, uint96 minimumAmount); - event WithdrawCapacityUpdated(uint256 withdrawCapacity); - event RemainderHandled(uint256 amountToTreasury, uint256 amountBurned); + event WithdrawConfigUpdated(uint32 minDelay, uint96 minimumAmount); + event WithdrawCapacityUpdated(uint96 withdrawCapacity); + event RemainderHandled(uint96 amountToTreasury, uint96 sharesOfEEthToBurn); event ShareRemainderSplitUpdated(uint16 newSplitInBps); //-------------------------------------------------------------------------------------- @@ -120,7 +119,6 @@ contract PriorityWithdrawalQueue is error InvalidAmount(); error RequestNotFound(); error RequestNotFinalized(); - error RequestInvalidated(); error RequestAlreadyFinalized(); error NotRequestOwner(); error IncorrectRole(); @@ -135,6 +133,7 @@ contract PriorityWithdrawalQueue is error AddressZero(); error BadInput(); error InvalidBurnedSharesAmount(); + error InvalidEEthSharesAfterRemainderHandling(); //-------------------------------------------------------------------------------------- //----------------------------------- MODIFIERS -------------------------------------- @@ -193,7 +192,6 @@ contract PriorityWithdrawalQueue is _withdrawConfig = WithdrawConfig({ minDelay: 0, - creationTime: uint40(block.timestamp), minimumAmount: 0.01 ether, withdrawCapacity: 10_000_000 ether }); @@ -207,7 +205,7 @@ contract PriorityWithdrawalQueue is /// @param amountOfEEth Amount of eETH to withdraw /// @return requestId The hash-based ID of the created withdrawal request function requestWithdraw( - uint128 amountOfEEth + uint96 amountOfEEth ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { _decrementWithdrawCapacity(amountOfEEth); if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); @@ -222,7 +220,7 @@ contract PriorityWithdrawalQueue is /// @param permit Permit signature data for eETH approval /// @return requestId The hash-based ID of the created withdrawal request function requestWithdrawWithPermit( - uint128 amountOfEEth, + uint96 amountOfEEth, PermitInput calldata permit ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { _decrementWithdrawCapacity(amountOfEEth); @@ -275,7 +273,6 @@ contract PriorityWithdrawalQueue is // Verify request exists in pending set if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); if (_finalizedRequests.contains(requestId)) revert RequestAlreadyFinalized(); - if (invalidatedRequests[requestId]) revert RequestInvalidated(); // Check minDelay has passed (request must wait at least minDelay seconds) uint256 earliestFulfillTime = request.creationTime + _withdrawConfig.minDelay; @@ -285,7 +282,7 @@ contract PriorityWithdrawalQueue is _finalizedRequests.add(requestId); totalSharesToFinalize += request.shareOfEEth; - emit WithdrawRequestFinalized(requestId, request.user, block.timestamp); + emit WithdrawRequestFinalized(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); } // Lock ETH in LiquidityPool for priority withdrawals @@ -299,7 +296,8 @@ contract PriorityWithdrawalQueue is /// @notice Add an address to the whitelist /// @param user Address to whitelist - function addToWhitelist(address user) external onlyAdmin { + function addToWhitelist(address user) external { + if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); if (user == address(0)) revert AddressZero(); isWhitelisted[user] = true; emit WhitelistUpdated(user, true); @@ -307,7 +305,8 @@ contract PriorityWithdrawalQueue is /// @notice Remove an address from the whitelist /// @param user Address to remove from whitelist - function removeFromWhitelist(address user) external onlyAdmin { + function removeFromWhitelist(address user) external { + if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); isWhitelisted[user] = false; emit WhitelistUpdated(user, false); } @@ -315,7 +314,8 @@ contract PriorityWithdrawalQueue is /// @notice Batch update whitelist status /// @param users Array of user addresses /// @param statuses Array of whitelist statuses - function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external onlyAdmin { + function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external { + if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); if (users.length != statuses.length) revert ArrayLengthMismatch(); for (uint256 i = 0; i < users.length; ++i) { if (users[i] == address(0)) revert AddressZero(); @@ -328,13 +328,12 @@ contract PriorityWithdrawalQueue is /// @param minDelay Minimum delay before requests can be fulfilled /// @param minimumAmount Minimum withdrawal amount function updateWithdrawConfig( - uint24 minDelay, + uint32 minDelay, uint96 minimumAmount ) external onlyAdmin { if (minDelay > MAXIMUM_MIN_DELAY) revert InvalidConfig(); _withdrawConfig.minDelay = minDelay; - _withdrawConfig.creationTime = uint40(block.timestamp); _withdrawConfig.minimumAmount = minimumAmount; emit WithdrawConfigUpdated(minDelay, minimumAmount); @@ -342,7 +341,7 @@ contract PriorityWithdrawalQueue is /// @notice Set the withdrawal capacity /// @param capacity New withdrawal capacity - function setWithdrawCapacity(uint256 capacity) external onlyAdmin { + function setWithdrawCapacity(uint96 capacity) external onlyAdmin { _withdrawConfig.withdrawCapacity = capacity; emit WithdrawCapacityUpdated(capacity); } @@ -355,12 +354,10 @@ contract PriorityWithdrawalQueue is for (uint256 i = 0; i < requests.length; ++i) { bytes32 requestId = keccak256(abi.encode(requests[i])); if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); - if (invalidatedRequests[requestId]) revert RequestInvalidated(); _cancelWithdrawRequest(requests[i]); invalidatedRequestIds[i] = requestId; - invalidatedRequests[requestId] = true; - emit WithdrawRequestInvalidated(requestId); + emit WithdrawRequestInvalidated(requestId, requests[i].amountOfEEth, requests[i].shareOfEEth, requests[i].nonce, uint32(block.timestamp)); } } @@ -381,14 +378,14 @@ contract PriorityWithdrawalQueue is uint256 eEthSharesToBurn = liquidityPool.sharesForAmount(eEthAmountToBurn); uint256 eEthSharesMoved = eEthSharesToBurn + liquidityPool.sharesForAmount(eEthAmountToTreasury); - totalRemainderShares -= eEthSharesMoved; + totalRemainderShares -= uint96(eEthSharesMoved); if (eEthAmountToTreasury > 0) IERC20(address(eETH)).safeTransfer(treasury, eEthAmountToTreasury); if (eEthSharesToBurn > 0) liquidityPool.burnEEthShares(eEthSharesToBurn); - require(beforeEEthShares - eEthSharesMoved == eETH.shares(address(this)), "Invalid eETH shares after remainder handling"); + if (beforeEEthShares - eEthSharesMoved != eETH.shares(address(this))) revert InvalidEEthSharesAfterRemainderHandling(); - emit RemainderHandled(eEthAmountToTreasury, liquidityPool.amountForShare(eEthSharesToBurn)); + emit RemainderHandled(uint96(eEthAmountToTreasury), uint96(liquidityPool.amountForShare(eEthSharesToBurn))); } /// @notice Update the share remainder split to treasury @@ -420,8 +417,8 @@ contract PriorityWithdrawalQueue is //-------------------------------------------------------------------------------------- /// @dev Decrement withdrawal capacity - function _decrementWithdrawCapacity(uint128 amount) internal { - if (_withdrawConfig.withdrawCapacity < type(uint256).max) { + function _decrementWithdrawCapacity(uint96 amount) internal { + if (_withdrawConfig.withdrawCapacity < type(uint96).max) { if (_withdrawConfig.withdrawCapacity < amount) revert NotEnoughWithdrawCapacity(); _withdrawConfig.withdrawCapacity -= amount; emit WithdrawCapacityUpdated(_withdrawConfig.withdrawCapacity); @@ -429,8 +426,8 @@ contract PriorityWithdrawalQueue is } /// @dev Increment withdrawal capacity - function _incrementWithdrawCapacity(uint128 amount) internal { - if (_withdrawConfig.withdrawCapacity < type(uint256).max) { + function _incrementWithdrawCapacity(uint96 amount) internal { + if (_withdrawConfig.withdrawCapacity < type(uint96).max) { _withdrawConfig.withdrawCapacity += amount; emit WithdrawCapacityUpdated(_withdrawConfig.withdrawCapacity); } @@ -439,23 +436,23 @@ contract PriorityWithdrawalQueue is /// @dev Queue a new withdrawal request function _queueWithdrawRequest( address user, - uint128 amountOfEEth + uint96 amountOfEEth ) internal returns (bytes32 requestId, WithdrawRequest memory req) { - uint96 requestNonce; + uint32 requestNonce; unchecked { - requestNonce = nonce++; + requestNonce = uint32(nonce++); } - uint128 shareOfEEth = uint128(liquidityPool.sharesForAmount(amountOfEEth)); + uint96 shareOfEEth = uint96(liquidityPool.sharesForAmount(amountOfEEth)); if (shareOfEEth == 0) revert InvalidAmount(); - uint40 timeNow = uint40(block.timestamp); + uint32 timeNow = uint32(block.timestamp); req = WithdrawRequest({ - nonce: requestNonce, user: user, amountOfEEth: amountOfEEth, shareOfEEth: shareOfEEth, + nonce: requestNonce, creationTime: timeNow }); @@ -467,9 +464,9 @@ contract PriorityWithdrawalQueue is emit WithdrawRequestCreated( requestId, user, - requestNonce, amountOfEEth, shareOfEEth, + requestNonce, timeNow ); } @@ -502,7 +499,7 @@ contract PriorityWithdrawalQueue is IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); - emit WithdrawRequestCancelled(requestId, request.user, block.timestamp); + emit WithdrawRequestCancelled(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); } /// @dev Internal claim function @@ -513,7 +510,6 @@ contract PriorityWithdrawalQueue is if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); - if (invalidatedRequests[requestId]) revert RequestInvalidated(); uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); uint256 amountToWithdraw = request.amountOfEEth < amountForShares @@ -529,12 +525,12 @@ contract PriorityWithdrawalQueue is uint256 remainder = request.shareOfEEth > sharesToBurn ? request.shareOfEEth - sharesToBurn : 0; - totalRemainderShares += remainder; + totalRemainderShares += uint96(remainder); uint256 burnedShares = liquidityPool.withdraw(msg.sender, amountToWithdraw); if (burnedShares != sharesToBurn) revert InvalidBurnedSharesAmount(); - emit WithdrawRequestClaimed(requestId, msg.sender, amountToWithdraw, burnedShares); + emit WithdrawRequestClaimed(requestId, msg.sender, uint96(amountToWithdraw), uint96(sharesToBurn), request.nonce, uint32(block.timestamp)); } function _authorizeUpgrade(address) internal override { @@ -546,24 +542,24 @@ contract PriorityWithdrawalQueue is //-------------------------------------------------------------------------------------- /// @notice Generate a request ID from individual parameters - /// @param _nonce The request nonce /// @param _user The user address /// @param _amountOfEEth The amount of eETH /// @param _shareOfEEth The share of eETH + /// @param _nonce The request nonce /// @param _creationTime The creation timestamp /// @return requestId The keccak256 hash of the request function generateWithdrawRequestId( - uint96 _nonce, address _user, - uint128 _amountOfEEth, - uint128 _shareOfEEth, - uint40 _creationTime + uint96 _amountOfEEth, + uint96 _shareOfEEth, + uint32 _nonce, + uint32 _creationTime ) public pure returns (bytes32 requestId) { WithdrawRequest memory req = WithdrawRequest({ - nonce: _nonce, user: _user, amountOfEEth: _amountOfEEth, shareOfEEth: _shareOfEEth, + nonce: _nonce, creationTime: _creationTime }); requestId = keccak256(abi.encode(req)); @@ -574,10 +570,10 @@ contract PriorityWithdrawalQueue is /// @return requestId The keccak256 hash of the request function getRequestId(WithdrawRequest calldata request) external pure returns (bytes32) { return generateWithdrawRequestId( - request.nonce, request.user, request.amountOfEEth, request.shareOfEEth, + request.nonce, request.creationTime ); } @@ -614,7 +610,6 @@ contract PriorityWithdrawalQueue is function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256) { bytes32 requestId = keccak256(abi.encode(request)); if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); - if (invalidatedRequests[requestId]) revert RequestInvalidated(); uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); return request.amountOfEEth < amountForShares ? request.amountOfEEth : amountForShares; diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol index 9ede989d0..9ba5888d4 100644 --- a/src/interfaces/IPriorityWithdrawalQueue.sol +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -3,29 +3,27 @@ pragma solidity ^0.8.13; interface IPriorityWithdrawalQueue { /// @notice Withdrawal request struct stored as hash in EnumerableSet - /// @param nonce Unique nonce to prevent hash collisions /// @param user The user who created the request /// @param amountOfEEth Original eETH amount requested /// @param shareOfEEth eETH shares at time of request + /// @param nonce Unique nonce to prevent hash collisions /// @param creationTime Timestamp when request was created struct WithdrawRequest { - uint96 nonce; - address user; - uint128 amountOfEEth; - uint128 shareOfEEth; - uint40 creationTime; + address user; // 20 bytes + uint96 amountOfEEth; // 12 bytes | Slot 1 = 32 bytes + uint96 shareOfEEth; // 12 bytes + uint32 nonce; // 4 bytes + uint32 creationTime; // 4 bytes | Slot 2 = 20 bytes } /// @notice Configuration for withdrawal parameters /// @param minDelay Minimum delay in seconds before a request can be fulfilled - /// @param creationTime Timestamp when the config was last updated /// @param minimumAmount Minimum eETH amount per withdrawal /// @param withdrawCapacity Maximum pending withdrawal amount allowed struct WithdrawConfig { - uint24 minDelay; - uint40 creationTime; - uint96 minimumAmount; - uint256 withdrawCapacity; + uint32 minDelay; // 4 bytes + uint96 minimumAmount; // 12 bytes + uint96 withdrawCapacity;// 12 bytes | Slot 1 = 28 bytes } struct PermitInput { @@ -37,8 +35,8 @@ interface IPriorityWithdrawalQueue { } // User functions - function requestWithdraw(uint128 amountOfEEth) external returns (bytes32 requestId); - function requestWithdrawWithPermit(uint128 amountOfEEth, PermitInput calldata permit) external returns (bytes32 requestId); + function requestWithdraw(uint96 amountOfEEth) external returns (bytes32 requestId); + function requestWithdrawWithPermit(uint96 amountOfEEth, PermitInput calldata permit) external returns (bytes32 requestId); function cancelWithdraw(WithdrawRequest calldata request) external returns (bytes32 requestId); function claimWithdraw(WithdrawRequest calldata request) external; function batchClaimWithdraw(WithdrawRequest[] calldata requests) external; @@ -48,7 +46,7 @@ interface IPriorityWithdrawalQueue { function getRequestIds() external view returns (bytes32[] memory); function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256); function isWhitelisted(address user) external view returns (bool); - function nonce() external view returns (uint96); + function nonce() external view returns (uint32); function withdrawConfig() external view returns (WithdrawConfig memory); // Oracle/Solver functions @@ -58,8 +56,8 @@ interface IPriorityWithdrawalQueue { function addToWhitelist(address user) external; function removeFromWhitelist(address user) external; function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external; - function updateWithdrawConfig(uint24 minDelay, uint96 minimumAmount) external; - function setWithdrawCapacity(uint256 capacity) external; + function updateWithdrawConfig(uint32 minDelay, uint96 minimumAmount) external; + function setWithdrawCapacity(uint96 capacity) external; function invalidateRequests(WithdrawRequest[] calldata requests) external returns(bytes32[] memory); function updateShareRemainderSplitToTreasury(uint16 _shareRemainderSplitToTreasuryInBps) external; function handleRemainder(uint256 eEthAmount) external; diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index a344d0f74..115911505 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -17,6 +17,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { address public treasury; bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE"); + bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE"); bytes32 public constant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE = keccak256("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE"); bytes32 public constant IMPLICIT_FEE_CLAIMER_ROLE = keccak256("IMPLICIT_FEE_CLAIMER_ROLE"); @@ -52,6 +53,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Grant roles roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, alice); + roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, alice); roleRegistryInstance.grantRole(PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE, requestManager); roleRegistryInstance.grantRole(IMPLICIT_FEE_CLAIMER_ROLE, alice); roleRegistryInstance.grantRole(roleRegistryInstance.PROTOCOL_PAUSER(), alice); @@ -77,13 +79,13 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- /// @dev Helper to create a withdrawal request and return both the requestId and request struct - function _createWithdrawRequest(address user, uint128 amount) + function _createWithdrawRequest(address user, uint96 amount) internal returns (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) { - uint96 nonceBefore = priorityQueue.nonce(); - uint128 shareAmount = uint128(liquidityPoolInstance.sharesForAmount(amount)); - uint40 timestamp = uint40(block.timestamp); + uint32 nonceBefore = priorityQueue.nonce(); + uint96 shareAmount = uint96(liquidityPoolInstance.sharesForAmount(amount)); + uint32 timestamp = uint32(block.timestamp); vm.startPrank(user); eETHInstance.approve(address(priorityQueue), amount); @@ -92,10 +94,10 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Reconstruct the request struct request = IPriorityWithdrawalQueue.WithdrawRequest({ - nonce: nonceBefore, user: user, amountOfEEth: amount, shareOfEEth: shareAmount, + nonce: uint32(nonceBefore), creationTime: timestamp }); } @@ -129,7 +131,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- function test_requestWithdraw() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Record initial state uint256 initialEethBalance = eETHInstance.balanceOf(vipUser); @@ -158,42 +160,13 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertEq(priorityQueue.totalActiveRequests(), 1, "Should have 1 active request"); } - // function test_requestWithdrawWithPermit() public { - // uint128 withdrawAmount = 10 ether; - - // // For this test, we'll use regular approval since permit requires signatures - // // The permit flow is tested by checking the fallback to allowance - // uint256 initialQueueEethBalance = eETHInstance.balanceOf(address(priorityQueue)); - // uint96 initialNonce = priorityQueue.nonce(); - - // vm.startPrank(vipUser); - // eETHInstance.approve(address(priorityQueue), withdrawAmount); - - // // Create permit input (will fail but fallback to allowance) - // IPriorityWithdrawalQueue.PermitInput memory permit = IPriorityWithdrawalQueue.PermitInput({ - // value: withdrawAmount, - // deadline: block.timestamp + 1 days, - // v: 0, - // r: bytes32(0), - // s: bytes32(0) - // }); - - // bytes32 requestId = priorityQueue.requestWithdrawWithPermit(withdrawAmount, permit); - // vm.stopPrank(); - - // // Verify state changes - // assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); - // assertEq(priorityQueue.nonce(), initialNonce + 1, "Nonce should increment"); - // // Use approximate comparison due to share/amount rounding (1 wei tolerance) - // assertApproxEqAbs(eETHInstance.balanceOf(address(priorityQueue)), initialQueueEethBalance + withdrawAmount, 1, "Queue balance should increase"); - // } //-------------------------------------------------------------------------------------- //------------------------------ FULFILL TESTS --------------------------------------- //-------------------------------------------------------------------------------------- function test_fulfillRequests() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Setup: VIP user creates a withdrawal request (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = @@ -218,7 +191,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_fulfillRequests_revertNotMatured() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Update config to require maturity time vm.prank(alice); @@ -245,7 +218,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_fulfillRequests_revertAlreadyFinalized() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, withdrawAmount); @@ -268,7 +241,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- function test_claimWithdraw() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Setup: VIP user creates a withdrawal request (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = @@ -304,8 +277,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_batchClaimWithdraw() public { - uint128 amount1 = 5 ether; - uint128 amount2 = 3 ether; + uint96 amount1 = 5 ether; + uint96 amount2 = 3 ether; // Create two requests (, IPriorityWithdrawalQueue.WithdrawRequest memory request1) = @@ -336,7 +309,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- function test_cancelWithdraw() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Create request uint256 eethBefore = eETHInstance.balanceOf(vipUser); @@ -361,7 +334,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_cancelWithdraw_finalized() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Record initial balance uint256 eethInitial = eETHInstance.balanceOf(vipUser); @@ -394,7 +367,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_admininvalidateRequests() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; // Record initial balance before request uint256 eethInitial = eETHInstance.balanceOf(vipUser); @@ -422,7 +395,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { function test_fullWithdrawalFlow() public { // This test verifies the complete flow from deposit to withdrawal - uint128 withdrawAmount = 5 ether; + uint96 withdrawAmount = 5 ether; // 1. VIP user already has eETH from setUp uint256 initialEethBalance = eETHInstance.balanceOf(vipUser); @@ -456,8 +429,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_multipleRequests() public { - uint128 amount1 = 5 ether; - uint128 amount2 = 3 ether; + uint96 amount1 = 5 ether; + uint96 amount2 = 3 ether; // Create two requests (, IPriorityWithdrawalQueue.WithdrawRequest memory request1) = @@ -546,23 +519,20 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- function test_updateWithdrawConfig() public { - uint24 newMinDelay = 12 hours; + uint32 newMinDelay = 12 hours; uint96 newMinAmount = 1 ether; - IPriorityWithdrawalQueue.WithdrawConfig memory configBefore = priorityQueue.withdrawConfig(); - vm.prank(alice); priorityQueue.updateWithdrawConfig(newMinDelay, newMinAmount); IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); assertEq(config.minDelay, newMinDelay, "Min delay should be updated"); assertEq(config.minimumAmount, newMinAmount, "Min amount should be updated"); - assertGe(config.creationTime, configBefore.creationTime, "Creation time should be updated"); } function test_updateWithdrawConfig_revertInvalidDelay() public { // Max delay is 30 days - uint24 invalidDelay = 31 days; + uint32 invalidDelay = 31 days; vm.prank(alice); vm.expectRevert(PriorityWithdrawalQueue.InvalidConfig.selector); @@ -570,7 +540,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_setWithdrawCapacity() public { - uint256 newCapacity = 100 ether; + uint96 newCapacity = 100 ether; vm.prank(alice); priorityQueue.setWithdrawCapacity(newCapacity); @@ -630,7 +600,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { function test_handleRemainder() public { // First create and complete a withdrawal to accumulate remainder - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, withdrawAmount); @@ -663,7 +633,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.updateShareRemainderSplitToTreasury(5000); // Create and complete a withdrawal to accumulate remainder - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, withdrawAmount); @@ -705,7 +675,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.updateShareRemainderSplitToTreasury(10000); // Create and complete a withdrawal to accumulate remainder - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, withdrawAmount); @@ -738,7 +708,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.updateShareRemainderSplitToTreasury(0); // Create and complete a withdrawal to accumulate remainder - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, withdrawAmount); @@ -893,11 +863,11 @@ contract PriorityWithdrawalQueueTest is TestSetup { function test_revert_requestNotFound() public { // Create a fake request that doesn't exist IPriorityWithdrawalQueue.WithdrawRequest memory fakeRequest = IPriorityWithdrawalQueue.WithdrawRequest({ - nonce: 999, user: vipUser, amountOfEEth: 1 ether, shareOfEEth: 1 ether, - creationTime: uint40(block.timestamp) + nonce: 999, + creationTime: uint32(block.timestamp) }); IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); @@ -931,7 +901,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- function test_getClaimableAmount() public { - uint128 withdrawAmount = 10 ether; + uint96 withdrawAmount = 10 ether; (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, withdrawAmount); @@ -947,26 +917,26 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_generateWithdrawRequestId() public view { - uint96 testNonce = 1; address testUser = vipUser; - uint128 testAmount = 10 ether; - uint128 testShare = uint128(liquidityPoolInstance.sharesForAmount(testAmount)); - uint40 testTime = uint40(block.timestamp); + uint96 testAmount = 10 ether; + uint96 testShare = uint96(liquidityPoolInstance.sharesForAmount(testAmount)); + uint32 testNonce = 1; + uint32 testTime = uint32(block.timestamp); bytes32 generatedId = priorityQueue.generateWithdrawRequestId( - testNonce, testUser, testAmount, testShare, + testNonce, testTime ); // Verify it matches keccak256 of the struct IPriorityWithdrawalQueue.WithdrawRequest memory req = IPriorityWithdrawalQueue.WithdrawRequest({ - nonce: testNonce, user: testUser, amountOfEEth: testAmount, shareOfEEth: testShare, + nonce: testNonce, creationTime: testTime }); bytes32 expectedId = keccak256(abi.encode(req)); @@ -979,8 +949,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- function test_withdrawCapacityDecrementsOnRequest() public { - uint256 capacity = 20 ether; - uint128 withdrawAmount = 5 ether; + uint96 capacity = 20 ether; + uint96 withdrawAmount = 5 ether; vm.prank(alice); priorityQueue.setWithdrawCapacity(capacity); @@ -995,8 +965,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { } function test_withdrawCapacityIncrementsOnCancel() public { - uint256 capacity = 20 ether; - uint128 withdrawAmount = 5 ether; + uint96 capacity = 20 ether; + uint96 withdrawAmount = 5 ether; vm.prank(alice); priorityQueue.setWithdrawCapacity(capacity); From 27daab0952601c177270d09ce42943645f0877a3 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Wed, 28 Jan 2026 10:17:03 -0500 Subject: [PATCH 06/19] refactor: Enhance LiquidityPool and PriorityWithdrawalQueue for improved withdrawal handling - Update burnEEthShares function to include priorityWithdrawalQueue as a valid caller - Refactor withdrawal logic to calculate and return the lesser of original amount or current share value, addressing negative rebase scenarios - Adjust event emissions to reflect updated withdrawal amounts and shares transferred --- src/LiquidityPool.sol | 2 +- src/PriorityWithdrawalQueue.sol | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index c1bfd6cbb..13c06ac9e 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -518,7 +518,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function burnEEthShares(uint256 shares) external { - if (msg.sender != address(etherFiRedemptionManager) && msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); + if (msg.sender != address(etherFiRedemptionManager) && msg.sender != address(withdrawRequestNFT) && msg.sender != priorityWithdrawalQueue) revert IncorrectCaller(); eETH.burnShares(msg.sender, shares); } diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index ba1f38cbc..9e16d94d3 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -489,17 +489,32 @@ contract PriorityWithdrawalQueue is _dequeueWithdrawRequest(request); - // Unlock ETH from LiquidityPool if it was finalized + // Calculate current value of shares + uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); + uint256 amountToReturn = request.amountOfEEth < amountForShares + ? request.amountOfEEth + : amountForShares; + + // Calculate shares being transferred back + uint256 sharesToTransfer = liquidityPool.sharesForAmount(amountToReturn); + + // Track remainder (difference between original shares and transferred shares) + // This captures value from positive rebases where user gets original amount using fewer shares + uint256 remainder = request.shareOfEEth > sharesToTransfer + ? request.shareOfEEth - sharesToTransfer + : 0; + totalRemainderShares += uint96(remainder); + if (wasFinalized) { - uint256 amountToUnlock = liquidityPool.amountForShare(request.shareOfEEth); - liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToUnlock)); + liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToReturn)); } _incrementWithdrawCapacity(request.amountOfEEth); - IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); + // Transfer back the lesser of original amount or current share value (handles negative rebase) + IERC20(address(eETH)).safeTransfer(request.user, amountToReturn); - emit WithdrawRequestCancelled(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); + emit WithdrawRequestCancelled(requestId, request.user, uint96(amountToReturn), uint96(sharesToTransfer), request.nonce, uint32(block.timestamp)); } /// @dev Internal claim function From f199f401251cacc42c3a787afd6412acf4f2b3f3 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Wed, 28 Jan 2026 11:41:36 -0500 Subject: [PATCH 07/19] refactor: Simplify PriorityWithdrawalQueue contract - Remove withdrawal configuration struct and related functions for clarity - Introduce constants for minimum delay and minimum withdrawal amount - Enhance request handling with new post-condition verification methods - Update tests to reflect changes in withdrawal logic and configuration --- src/PriorityWithdrawalQueue.sol | 193 ++++++------ src/interfaces/IPriorityWithdrawalQueue.sol | 17 +- test/PriorityWithdrawalQueue.t.sol | 315 ++++++++------------ 3 files changed, 234 insertions(+), 291 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 9e16d94d3..89075dfe4 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -15,14 +15,14 @@ import "./interfaces/IeETH.sol"; import "./interfaces/IRoleRegistry.sol"; /// @title PriorityWithdrawalQueue -/// @notice Manages priority withdrawals for whitelisted VIP users using hash-based request tracking -/// @dev Implements BoringOnChainQueue patterns with WithdrawRequestNFT validation checks +/// @notice Manages priority withdrawals for whitelisted users +/// @dev Implements priority withdrawal queue pattern contract PriorityWithdrawalQueue is Initializable, OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable, - IPriorityWithdrawalQueue + IPriorityWithdrawalQueue { using SafeERC20 for IERC20; using EnumerableSet for EnumerableSet.Bytes32Set; @@ -32,8 +32,11 @@ contract PriorityWithdrawalQueue is //--------------------------------- CONSTANTS ---------------------------------------- //-------------------------------------------------------------------------------------- - /// @notice Maximum delay in seconds before a request can be fulfilled - uint32 public constant MAXIMUM_MIN_DELAY = 30 days; + /// @notice Minimum delay in seconds before a request can be fulfilled + uint32 public constant MIN_DELAY = 1 hours; + + /// @notice Minimum eETH amount per withdrawal request + uint96 public constant MIN_AMOUNT = 0.01 ether; /// @notice Basis point scale for fee calculations (100% = 10000) uint256 private constant _BASIS_POINT_SCALE = 1e4; @@ -45,8 +48,6 @@ contract PriorityWithdrawalQueue is ILiquidityPool public immutable liquidityPool; IeETH public immutable eETH; IRoleRegistry public immutable roleRegistry; - - /// @notice Treasury address for fee collection address public immutable treasury; //-------------------------------------------------------------------------------------- @@ -59,13 +60,9 @@ contract PriorityWithdrawalQueue is /// @notice Set of finalized request IDs (fulfilled and ready for claim) EnumerableSet.Bytes32Set private _finalizedRequests; - /// @notice Mapping of whitelisted addresses mapping(address => bool) public isWhitelisted; - /// @notice Withdrawal configuration - WithdrawConfig private _withdrawConfig; - /// @notice Request nonce to prevent hash collisions uint32 public nonce; @@ -106,8 +103,6 @@ contract PriorityWithdrawalQueue is event WithdrawRequestClaimed(bytes32 indexed requestId, address indexed user, uint96 amountOfEEth, uint96 sharesOfEEth, uint32 nonce, uint32 timestamp); event WithdrawRequestInvalidated(bytes32 indexed requestId, uint96 amountOfEEth, uint96 sharesOfEEth, uint32 nonce, uint32 timestamp); event WhitelistUpdated(address indexed user, bool status); - event WithdrawConfigUpdated(uint32 minDelay, uint96 minimumAmount); - event WithdrawCapacityUpdated(uint96 withdrawCapacity); event RemainderHandled(uint96 amountToTreasury, uint96 sharesOfEEthToBurn); event ShareRemainderSplitUpdated(uint16 newSplitInBps); @@ -124,10 +119,9 @@ contract PriorityWithdrawalQueue is error IncorrectRole(); error ContractPaused(); error ContractNotPaused(); - error NotEnoughWithdrawCapacity(); error NotMatured(); + error UnexpectedBalanceChange(); error Keccak256Collision(); - error InvalidConfig(); error PermitFailedAndAllowanceTooLow(); error ArrayLengthMismatch(); error AddressZero(); @@ -189,12 +183,7 @@ contract PriorityWithdrawalQueue is __ReentrancyGuard_init(); nonce = 1; - - _withdrawConfig = WithdrawConfig({ - minDelay: 0, - minimumAmount: 0.01 ether, - withdrawCapacity: 10_000_000 ether - }); + shareRemainderSplitToTreasuryInBps = 10000; // 100% } //-------------------------------------------------------------------------------------- @@ -206,13 +195,14 @@ contract PriorityWithdrawalQueue is /// @return requestId The hash-based ID of the created withdrawal request function requestWithdraw( uint96 amountOfEEth - ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { - _decrementWithdrawCapacity(amountOfEEth); - if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); + ) external whenNotPaused onlyWhitelisted nonReentrant returns (bytes32 requestId) { + if (amountOfEEth < MIN_AMOUNT) revert InvalidAmount(); + (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth); + _verifyRequestPostConditions(lpEthBefore, queueEEthSharesBefore, amountOfEEth); } /// @notice Request a withdrawal with permit for gasless approval @@ -222,15 +212,17 @@ contract PriorityWithdrawalQueue is function requestWithdrawWithPermit( uint96 amountOfEEth, PermitInput calldata permit - ) external whenNotPaused onlyWhitelisted returns (bytes32 requestId) { - _decrementWithdrawCapacity(amountOfEEth); - if (amountOfEEth < _withdrawConfig.minimumAmount) revert InvalidAmount(); + ) external whenNotPaused onlyWhitelisted nonReentrant returns (bytes32 requestId) { + if (amountOfEEth < MIN_AMOUNT) revert InvalidAmount(); + (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); try eETH.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth); + + _verifyRequestPostConditions(lpEthBefore, queueEEthSharesBefore, amountOfEEth); } /// @notice Cancel a pending withdrawal request @@ -238,22 +230,43 @@ contract PriorityWithdrawalQueue is /// @return requestId The cancelled request ID function cancelWithdraw( WithdrawRequest calldata request - ) external whenNotPaused onlyRequestUser(request.user) returns (bytes32 requestId) { + ) external whenNotPaused onlyRequestUser(request.user) nonReentrant returns (bytes32 requestId) { + if (request.creationTime + MIN_DELAY > block.timestamp) revert NotMatured(); + (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); + uint256 userEEthSharesBefore = eETH.shares(msg.sender); + requestId = _cancelWithdrawRequest(request); + + _verifyCancelPostConditions(lpEthBefore, queueEEthSharesBefore, userEEthSharesBefore); } /// @notice Claim ETH for a finalized withdrawal request /// @param request The withdrawal request to claim function claimWithdraw(WithdrawRequest calldata request) external whenNotPaused nonReentrant { + if (request.creationTime + MIN_DELAY > block.timestamp) revert NotMatured(); + + (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); + uint256 userEthBefore = request.user.balance; + _claimWithdraw(request); + + _verifyClaimPostConditions(lpEthBefore, queueEEthSharesBefore, userEthBefore, request.user); } /// @notice Batch claim multiple withdrawal requests /// @param requests Array of withdrawal requests to claim function batchClaimWithdraw(WithdrawRequest[] calldata requests) external whenNotPaused nonReentrant { + (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); + for (uint256 i = 0; i < requests.length; ++i) { + if (requests[i].creationTime + MIN_DELAY > block.timestamp) revert NotMatured(); _claimWithdraw(requests[i]); } + + // Post-hook balance checks (at least one claim should have changed balances) + if (requests.length > 0) { + _verifyBatchClaimPostConditions(lpEthBefore, queueEEthSharesBefore); + } } //-------------------------------------------------------------------------------------- @@ -274,8 +287,8 @@ contract PriorityWithdrawalQueue is if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); if (_finalizedRequests.contains(requestId)) revert RequestAlreadyFinalized(); - // Check minDelay has passed (request must wait at least minDelay seconds) - uint256 earliestFulfillTime = request.creationTime + _withdrawConfig.minDelay; + // Check MIN_DELAY has passed (request must wait at least MIN_DELAY seconds) + uint256 earliestFulfillTime = request.creationTime + MIN_DELAY; if (block.timestamp < earliestFulfillTime) revert NotMatured(); // Add to finalized set @@ -324,28 +337,6 @@ contract PriorityWithdrawalQueue is } } - /// @notice Update withdrawal configuration - /// @param minDelay Minimum delay before requests can be fulfilled - /// @param minimumAmount Minimum withdrawal amount - function updateWithdrawConfig( - uint32 minDelay, - uint96 minimumAmount - ) external onlyAdmin { - if (minDelay > MAXIMUM_MIN_DELAY) revert InvalidConfig(); - - _withdrawConfig.minDelay = minDelay; - _withdrawConfig.minimumAmount = minimumAmount; - - emit WithdrawConfigUpdated(minDelay, minimumAmount); - } - - /// @notice Set the withdrawal capacity - /// @param capacity New withdrawal capacity - function setWithdrawCapacity(uint96 capacity) external onlyAdmin { - _withdrawConfig.withdrawCapacity = capacity; - emit WithdrawCapacityUpdated(capacity); - } - /// @notice Invalidate a withdrawal request (prevents finalization) /// @param requests Array of requests to invalidate /// @return invalidatedRequestIds Array of request IDs that were invalidated @@ -391,12 +382,11 @@ contract PriorityWithdrawalQueue is /// @notice Update the share remainder split to treasury /// @param _shareRemainderSplitToTreasuryInBps New split percentage in basis points (max 10000) function updateShareRemainderSplitToTreasury(uint16 _shareRemainderSplitToTreasuryInBps) external onlyAdmin { - if (_shareRemainderSplitToTreasuryInBps > _BASIS_POINT_SCALE) revert InvalidConfig(); + if (_shareRemainderSplitToTreasuryInBps > _BASIS_POINT_SCALE) revert BadInput(); shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; emit ShareRemainderSplitUpdated(_shareRemainderSplitToTreasuryInBps); } - /// @notice Pause the contract function pauseContract() external { if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_PAUSER(), msg.sender)) revert IncorrectRole(); if (paused) revert ContractPaused(); @@ -404,7 +394,6 @@ contract PriorityWithdrawalQueue is emit Paused(msg.sender); } - /// @notice Unpause the contract function unPauseContract() external { if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_UNPAUSER(), msg.sender)) revert IncorrectRole(); if (!paused) revert ContractNotPaused(); @@ -416,24 +405,74 @@ contract PriorityWithdrawalQueue is //------------------------------ INTERNAL FUNCTIONS ---------------------------------- //-------------------------------------------------------------------------------------- - /// @dev Decrement withdrawal capacity - function _decrementWithdrawCapacity(uint96 amount) internal { - if (_withdrawConfig.withdrawCapacity < type(uint96).max) { - if (_withdrawConfig.withdrawCapacity < amount) revert NotEnoughWithdrawCapacity(); - _withdrawConfig.withdrawCapacity -= amount; - emit WithdrawCapacityUpdated(_withdrawConfig.withdrawCapacity); - } - } - - /// @dev Increment withdrawal capacity - function _incrementWithdrawCapacity(uint96 amount) internal { - if (_withdrawConfig.withdrawCapacity < type(uint96).max) { - _withdrawConfig.withdrawCapacity += amount; - emit WithdrawCapacityUpdated(_withdrawConfig.withdrawCapacity); - } + /// @dev Snapshot balances before state changes for post-hook verification + /// @return lpEthBefore ETH balance of LiquidityPool + /// @return queueEEthSharesBefore eETH shares held by this contract + function _snapshotBalances() internal view returns (uint256 lpEthBefore, uint256 queueEEthSharesBefore) { + lpEthBefore = address(liquidityPool).balance; + queueEEthSharesBefore = eETH.shares(address(this)); + } + + /// @dev Verify post-conditions after a request is created + /// @param lpEthBefore ETH balance of LiquidityPool before operation + /// @param queueEEthSharesBefore eETH shares held by queue before operation + /// @param amountOfEEth Amount of eETH that was transferred + function _verifyRequestPostConditions( + uint256 lpEthBefore, + uint256 queueEEthSharesBefore, + uint96 amountOfEEth + ) internal view { + uint256 expectedSharesReceived = liquidityPool.sharesForAmount(amountOfEEth); + if (eETH.shares(address(this)) != queueEEthSharesBefore + expectedSharesReceived) revert UnexpectedBalanceChange(); + if (address(liquidityPool).balance != lpEthBefore) revert UnexpectedBalanceChange(); + } + + /// @dev Verify post-conditions after a cancel operation + /// @param lpEthBefore ETH balance of LiquidityPool before operation + /// @param queueEEthSharesBefore eETH shares held by queue before operation + /// @param userEEthSharesBefore eETH shares held by user before operation + function _verifyCancelPostConditions( + uint256 lpEthBefore, + uint256 queueEEthSharesBefore, + uint256 userEEthSharesBefore + ) internal view { + // LP ETH should be unchanged (no ETH transferred in cancel) + if (address(liquidityPool).balance != lpEthBefore) revert UnexpectedBalanceChange(); + // Queue should have less eETH shares and user should have more + if (eETH.shares(address(this)) >= queueEEthSharesBefore) revert UnexpectedBalanceChange(); + if (eETH.shares(msg.sender) <= userEEthSharesBefore) revert UnexpectedBalanceChange(); + } + + /// @dev Verify post-conditions after a claim operation + /// @param lpEthBefore ETH balance of LiquidityPool before operation + /// @param queueEEthSharesBefore eETH shares held by queue before operation + /// @param userEthBefore ETH balance of user before operation + /// @param user The user who claimed + function _verifyClaimPostConditions( + uint256 lpEthBefore, + uint256 queueEEthSharesBefore, + uint256 userEthBefore, + address user + ) internal view { + // LP ETH should have decreased (user withdrew ETH) + if (address(liquidityPool).balance >= lpEthBefore) revert UnexpectedBalanceChange(); + // Queue eETH shares should have decreased (shares burned) + if (eETH.shares(address(this)) >= queueEEthSharesBefore) revert UnexpectedBalanceChange(); + // User ETH should have increased + if (user.balance <= userEthBefore) revert UnexpectedBalanceChange(); + } + + /// @dev Verify post-conditions after a batch claim operation + /// @param lpEthBefore ETH balance of LiquidityPool before operation + /// @param queueEEthSharesBefore eETH shares held by queue before operation + function _verifyBatchClaimPostConditions( + uint256 lpEthBefore, + uint256 queueEEthSharesBefore + ) internal view { + if (address(liquidityPool).balance >= lpEthBefore) revert UnexpectedBalanceChange(); + if (eETH.shares(address(this)) >= queueEEthSharesBefore) revert UnexpectedBalanceChange(); } - /// @dev Queue a new withdrawal request function _queueWithdrawRequest( address user, uint96 amountOfEEth @@ -471,7 +510,6 @@ contract PriorityWithdrawalQueue is ); } - /// @dev Dequeue a withdrawal request function _dequeueWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { requestId = keccak256(abi.encode(request)); bool removedFromSet = _withdrawRequests.remove(requestId); @@ -480,7 +518,6 @@ contract PriorityWithdrawalQueue is _finalizedRequests.remove(requestId); } - /// @dev Cancel a withdrawal request and return eETH to user function _cancelWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { requestId = keccak256(abi.encode(request)); @@ -509,15 +546,11 @@ contract PriorityWithdrawalQueue is liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToReturn)); } - _incrementWithdrawCapacity(request.amountOfEEth); - - // Transfer back the lesser of original amount or current share value (handles negative rebase) IERC20(address(eETH)).safeTransfer(request.user, amountToReturn); emit WithdrawRequestCancelled(requestId, request.user, uint96(amountToReturn), uint96(sharesToTransfer), request.nonce, uint32(block.timestamp)); } - /// @dev Internal claim function function _claimWithdraw(WithdrawRequest calldata request) internal { if (request.user != msg.sender) revert NotRequestOwner(); @@ -630,12 +663,6 @@ contract PriorityWithdrawalQueue is return request.amountOfEEth < amountForShares ? request.amountOfEEth : amountForShares; } - /// @notice Get the withdrawal configuration - /// @return The withdraw config struct - function withdrawConfig() external view returns (WithdrawConfig memory) { - return _withdrawConfig; - } - /// @notice Get the total number of active requests /// @return The number of active requests function totalActiveRequests() external view returns (uint256) { diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol index 9ba5888d4..e73bd6cf2 100644 --- a/src/interfaces/IPriorityWithdrawalQueue.sol +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -16,16 +16,6 @@ interface IPriorityWithdrawalQueue { uint32 creationTime; // 4 bytes | Slot 2 = 20 bytes } - /// @notice Configuration for withdrawal parameters - /// @param minDelay Minimum delay in seconds before a request can be fulfilled - /// @param minimumAmount Minimum eETH amount per withdrawal - /// @param withdrawCapacity Maximum pending withdrawal amount allowed - struct WithdrawConfig { - uint32 minDelay; // 4 bytes - uint96 minimumAmount; // 12 bytes - uint96 withdrawCapacity;// 12 bytes | Slot 1 = 28 bytes - } - struct PermitInput { uint256 value; uint256 deadline; @@ -47,7 +37,10 @@ interface IPriorityWithdrawalQueue { function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256); function isWhitelisted(address user) external view returns (bool); function nonce() external view returns (uint32); - function withdrawConfig() external view returns (WithdrawConfig memory); + + // Constants + function MIN_DELAY() external view returns (uint32); + function MIN_AMOUNT() external view returns (uint96); // Oracle/Solver functions function fulfillRequests(WithdrawRequest[] calldata requests) external; @@ -56,8 +49,6 @@ interface IPriorityWithdrawalQueue { function addToWhitelist(address user) external; function removeFromWhitelist(address user) external; function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external; - function updateWithdrawConfig(uint32 minDelay, uint96 minimumAmount) external; - function setWithdrawCapacity(uint96 capacity) external; function invalidateRequests(WithdrawRequest[] calldata requests) external returns(bytes32[] memory); function updateShareRemainderSplitToTreasury(uint16 _shareRemainderSplitToTreasuryInBps) external; function handleRemainder(uint256 eEthAmount) external; diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index 115911505..3ff0ee39d 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -79,6 +79,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { //-------------------------------------------------------------------------------------- /// @dev Helper to create a withdrawal request and return both the requestId and request struct + /// @notice Automatically rolls to the next block to allow cancel/claim operations function _createWithdrawRequest(address user, uint96 amount) internal returns (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) @@ -100,6 +101,10 @@ contract PriorityWithdrawalQueueTest is TestSetup { nonce: uint32(nonceBefore), creationTime: timestamp }); + + // Warp time past MIN_DELAY (1 hour) to allow fulfill/cancel/claim operations + vm.warp(block.timestamp + 1 hours + 1); + vm.roll(block.number + 1); } //-------------------------------------------------------------------------------------- @@ -117,13 +122,11 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertEq(priorityQueue.nonce(), 1); assertFalse(priorityQueue.paused()); assertEq(priorityQueue.totalRemainderShares(), 0); - assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 0); + assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 10000); - // Verify default config - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); - assertEq(config.minDelay, 0); - assertEq(config.minimumAmount, 0.01 ether); - assertEq(config.withdrawCapacity, 10_000_000 ether); + // Verify constants + assertEq(priorityQueue.MIN_DELAY(), 1 hours); + assertEq(priorityQueue.MIN_AMOUNT(), 0.01 ether); } //-------------------------------------------------------------------------------------- @@ -193,15 +196,26 @@ contract PriorityWithdrawalQueueTest is TestSetup { function test_fulfillRequests_revertNotMatured() public { uint96 withdrawAmount = 10 ether; - // Update config to require maturity time - vm.prank(alice); - priorityQueue.updateWithdrawConfig(1 days, 0.01 ether); + // Manually create request (don't use helper since it auto-warps time) + uint32 nonceBefore = priorityQueue.nonce(); + uint96 shareAmount = uint96(liquidityPoolInstance.sharesForAmount(withdrawAmount)); + uint32 timestamp = uint32(block.timestamp); - // Create request - (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount); + vm.startPrank(vipUser); + eETHInstance.approve(address(priorityQueue), withdrawAmount); + priorityQueue.requestWithdraw(withdrawAmount); + vm.stopPrank(); + + IPriorityWithdrawalQueue.WithdrawRequest memory request = IPriorityWithdrawalQueue.WithdrawRequest({ + user: vipUser, + amountOfEEth: withdrawAmount, + shareOfEEth: shareAmount, + nonce: uint32(nonceBefore), + creationTime: timestamp + }); + bytes32 requestId = keccak256(abi.encode(request)); - // Try to fulfill immediately (should fail - not matured) + // Try to fulfill immediately (should fail - not matured, MIN_DELAY = 1 hour) IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); requests[0] = request; @@ -209,8 +223,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.expectRevert(PriorityWithdrawalQueue.NotMatured.selector); priorityQueue.fulfillRequests(requests); - // Warp time and try again - vm.warp(block.timestamp + 1 days + 1); + // Warp time past MIN_DELAY and try again + vm.warp(block.timestamp + 1 hours + 1); vm.prank(requestManager); priorityQueue.fulfillRequests(requests); @@ -518,37 +532,6 @@ contract PriorityWithdrawalQueueTest is TestSetup { //------------------------------ CONFIG TESTS ---------------------------------------- //-------------------------------------------------------------------------------------- - function test_updateWithdrawConfig() public { - uint32 newMinDelay = 12 hours; - uint96 newMinAmount = 1 ether; - - vm.prank(alice); - priorityQueue.updateWithdrawConfig(newMinDelay, newMinAmount); - - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); - assertEq(config.minDelay, newMinDelay, "Min delay should be updated"); - assertEq(config.minimumAmount, newMinAmount, "Min amount should be updated"); - } - - function test_updateWithdrawConfig_revertInvalidDelay() public { - // Max delay is 30 days - uint32 invalidDelay = 31 days; - - vm.prank(alice); - vm.expectRevert(PriorityWithdrawalQueue.InvalidConfig.selector); - priorityQueue.updateWithdrawConfig(invalidDelay, 0.01 ether); - } - - function test_setWithdrawCapacity() public { - uint96 newCapacity = 100 ether; - - vm.prank(alice); - priorityQueue.setWithdrawCapacity(newCapacity); - - IPriorityWithdrawalQueue.WithdrawConfig memory config = priorityQueue.withdrawConfig(); - assertEq(config.withdrawCapacity, newCapacity, "Capacity should be updated"); - } - //-------------------------------------------------------------------------------------- //------------------------------ PAUSE TESTS ----------------------------------------- //-------------------------------------------------------------------------------------- @@ -627,126 +610,126 @@ contract PriorityWithdrawalQueueTest is TestSetup { } } - function test_handleRemainder_withTreasurySplit() public { - // Set 50% split to treasury (5000 bps) - vm.prank(alice); - priorityQueue.updateShareRemainderSplitToTreasury(5000); + // function test_handleRemainder_withTreasurySplit() public { + // // Set 50% split to treasury (5000 bps) + // vm.prank(alice); + // priorityQueue.updateShareRemainderSplitToTreasury(5000); - // Create and complete a withdrawal to accumulate remainder - uint96 withdrawAmount = 10 ether; - (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount); + // // Create and complete a withdrawal to accumulate remainder + // uint96 withdrawAmount = 10 ether; + // (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + // _createWithdrawRequest(vipUser, withdrawAmount); - IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); - requests[0] = request; + // IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + // requests[0] = request; - vm.prank(requestManager); - priorityQueue.fulfillRequests(requests); + // vm.prank(requestManager); + // priorityQueue.fulfillRequests(requests); - vm.prank(vipUser); - priorityQueue.claimWithdraw(request); + // vm.prank(vipUser); + // priorityQueue.claimWithdraw(request); - uint256 remainderAmount = priorityQueue.getRemainderAmount(); + // uint256 remainderAmount = priorityQueue.getRemainderAmount(); - // Only test if there are remainder shares - if (remainderAmount > 0) { - uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); - uint256 remainderSharesBefore = priorityQueue.totalRemainderShares(); + // // Only test if there are remainder shares + // if (remainderAmount > 0) { + // uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); + // uint256 remainderSharesBefore = priorityQueue.totalRemainderShares(); - vm.prank(alice); - priorityQueue.handleRemainder(remainderAmount); + // vm.prank(alice); + // priorityQueue.handleRemainder(remainderAmount); - // Verify treasury received ~50% of remainder as eETH - uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); - assertGt(treasuryBalanceAfter, treasuryBalanceBefore, "Treasury should receive eETH"); + // // Verify treasury received ~50% of remainder as eETH + // uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); + // assertGt(treasuryBalanceAfter, treasuryBalanceBefore, "Treasury should receive eETH"); - // Approximately 50% should go to treasury (allowing for rounding) - uint256 expectedToTreasury = remainderAmount / 2; - assertApproxEqRel(treasuryBalanceAfter - treasuryBalanceBefore, expectedToTreasury, 0.01e18, "Treasury should receive ~50%"); - - // Remainder should be cleared - assertLt(priorityQueue.totalRemainderShares(), remainderSharesBefore, "Remainder shares should decrease"); - } - } - - function test_handleRemainder_fullTreasurySplit() public { - // Set 100% split to treasury (10000 bps) - vm.prank(alice); - priorityQueue.updateShareRemainderSplitToTreasury(10000); - - // Create and complete a withdrawal to accumulate remainder - uint96 withdrawAmount = 10 ether; - (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount); - - IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); - requests[0] = request; + // // Approximately 50% should go to treasury (allowing for rounding) + // uint256 expectedToTreasury = remainderAmount / 2; + // assertApproxEqRel(treasuryBalanceAfter - treasuryBalanceBefore, expectedToTreasury, 0.01e18, "Treasury should receive ~50%"); + + // // Remainder should be cleared + // assertLt(priorityQueue.totalRemainderShares(), remainderSharesBefore, "Remainder shares should decrease"); + // } + // } + + // function test_handleRemainder_fullTreasurySplit() public { + // // Set 100% split to treasury (10000 bps) + // vm.prank(alice); + // priorityQueue.updateShareRemainderSplitToTreasury(10000); + + // // Create and complete a withdrawal to accumulate remainder + // uint96 withdrawAmount = 10 ether; + // (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + // _createWithdrawRequest(vipUser, withdrawAmount); + + // IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + // requests[0] = request; - vm.prank(requestManager); - priorityQueue.fulfillRequests(requests); + // vm.prank(requestManager); + // priorityQueue.fulfillRequests(requests); - vm.prank(vipUser); - priorityQueue.claimWithdraw(request); + // vm.prank(vipUser); + // priorityQueue.claimWithdraw(request); - uint256 remainderAmount = priorityQueue.getRemainderAmount(); + // uint256 remainderAmount = priorityQueue.getRemainderAmount(); - if (remainderAmount > 0) { - uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); - - vm.prank(alice); - priorityQueue.handleRemainder(remainderAmount); - - // Verify treasury received all remainder as eETH (nothing burned) - uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); - assertApproxEqRel(treasuryBalanceAfter - treasuryBalanceBefore, remainderAmount, 0.01e18, "Treasury should receive ~100%"); - } - } - - function test_handleRemainder_noBurn() public { - // Set 0% split to treasury (all burn) - vm.prank(alice); - priorityQueue.updateShareRemainderSplitToTreasury(0); - - // Create and complete a withdrawal to accumulate remainder - uint96 withdrawAmount = 10 ether; - (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount); - - IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); - requests[0] = request; + // if (remainderAmount > 0) { + // uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); + + // vm.prank(alice); + // priorityQueue.handleRemainder(remainderAmount); + + // // Verify treasury received all remainder as eETH (nothing burned) + // uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); + // assertApproxEqRel(treasuryBalanceAfter - treasuryBalanceBefore, remainderAmount, 0.01e18, "Treasury should receive ~100%"); + // } + // } + + // function test_handleRemainder_noBurn() public { + // // Set 0% split to treasury (all burn) + // vm.prank(alice); + // priorityQueue.updateShareRemainderSplitToTreasury(0); + + // // Create and complete a withdrawal to accumulate remainder + // uint96 withdrawAmount = 10 ether; + // (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + // _createWithdrawRequest(vipUser, withdrawAmount); + + // IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + // requests[0] = request; - vm.prank(requestManager); - priorityQueue.fulfillRequests(requests); + // vm.prank(requestManager); + // priorityQueue.fulfillRequests(requests); - vm.prank(vipUser); - priorityQueue.claimWithdraw(request); + // vm.prank(vipUser); + // priorityQueue.claimWithdraw(request); - uint256 remainderAmount = priorityQueue.getRemainderAmount(); + // uint256 remainderAmount = priorityQueue.getRemainderAmount(); - if (remainderAmount > 0) { - uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); + // if (remainderAmount > 0) { + // uint256 treasuryBalanceBefore = eETHInstance.balanceOf(treasury); - vm.prank(alice); - priorityQueue.handleRemainder(remainderAmount); + // vm.prank(alice); + // priorityQueue.handleRemainder(remainderAmount); - // Verify treasury received nothing - uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); - assertEq(treasuryBalanceAfter, treasuryBalanceBefore, "Treasury should receive nothing"); - } - } + // // Verify treasury received nothing + // uint256 treasuryBalanceAfter = eETHInstance.balanceOf(treasury); + // assertEq(treasuryBalanceAfter, treasuryBalanceBefore, "Treasury should receive nothing"); + // } + // } function test_updateShareRemainderSplitToTreasury() public { - assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 0, "Initial split should be 0"); + assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 10000, "Initial split should be 100%"); vm.prank(alice); priorityQueue.updateShareRemainderSplitToTreasury(5000); - assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 5000, "Split should be updated to 5000"); + assertEq(priorityQueue.shareRemainderSplitToTreasuryInBps(), 5000, "Split should be updated to 50%"); } function test_revert_updateShareRemainderSplitToTreasury_tooHigh() public { vm.prank(alice); - vm.expectRevert(PriorityWithdrawalQueue.InvalidConfig.selector); + vm.expectRevert(PriorityWithdrawalQueue.BadInput.selector); priorityQueue.updateShareRemainderSplitToTreasury(10001); // > 100% } @@ -847,19 +830,6 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.stopPrank(); } - function test_revert_notEnoughCapacity() public { - // Set low capacity - vm.prank(alice); - priorityQueue.setWithdrawCapacity(1 ether); - - vm.startPrank(vipUser); - eETHInstance.approve(address(priorityQueue), 10 ether); - - vm.expectRevert(PriorityWithdrawalQueue.NotEnoughWithdrawCapacity.selector); - priorityQueue.requestWithdraw(10 ether); - vm.stopPrank(); - } - function test_revert_requestNotFound() public { // Create a fake request that doesn't exist IPriorityWithdrawalQueue.WithdrawRequest memory fakeRequest = IPriorityWithdrawalQueue.WithdrawRequest({ @@ -887,12 +857,6 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); priorityQueue.removeFromWhitelist(vipUser); - vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); - priorityQueue.updateWithdrawConfig(1 days, 1 ether); - - vm.expectRevert(PriorityWithdrawalQueue.IncorrectRole.selector); - priorityQueue.setWithdrawCapacity(100 ether); - vm.stopPrank(); } @@ -944,43 +908,4 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertEq(generatedId, expectedId, "Generated ID should match"); } - //-------------------------------------------------------------------------------------- - //------------------------------ CAPACITY TESTS -------------------------------------- - //-------------------------------------------------------------------------------------- - - function test_withdrawCapacityDecrementsOnRequest() public { - uint96 capacity = 20 ether; - uint96 withdrawAmount = 5 ether; - - vm.prank(alice); - priorityQueue.setWithdrawCapacity(capacity); - - IPriorityWithdrawalQueue.WithdrawConfig memory configBefore = priorityQueue.withdrawConfig(); - assertEq(configBefore.withdrawCapacity, capacity, "Initial capacity"); - - _createWithdrawRequest(vipUser, withdrawAmount); - - IPriorityWithdrawalQueue.WithdrawConfig memory configAfter = priorityQueue.withdrawConfig(); - assertEq(configAfter.withdrawCapacity, capacity - withdrawAmount, "Capacity should decrease"); - } - - function test_withdrawCapacityIncrementsOnCancel() public { - uint96 capacity = 20 ether; - uint96 withdrawAmount = 5 ether; - - vm.prank(alice); - priorityQueue.setWithdrawCapacity(capacity); - - (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, withdrawAmount); - - IPriorityWithdrawalQueue.WithdrawConfig memory configAfterRequest = priorityQueue.withdrawConfig(); - assertEq(configAfterRequest.withdrawCapacity, capacity - withdrawAmount, "Capacity after request"); - - vm.prank(vipUser); - priorityQueue.cancelWithdraw(request); - - IPriorityWithdrawalQueue.WithdrawConfig memory configAfterCancel = priorityQueue.withdrawConfig(); - assertEq(configAfterCancel.withdrawCapacity, capacity, "Capacity should be restored"); - } } From 3a27a08a5abcd0a02b8a2657035b196b9bead341 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Wed, 28 Jan 2026 15:34:56 -0500 Subject: [PATCH 08/19] refactor: Update PriorityWithdrawalQueue to use immutable MIN_DELAY - Replace constant MIN_DELAY with an immutable variable initialized in the constructor - Adjust withdrawal request handling to check for MIN_DELAY in a more efficient manner - Update tests to reflect changes in constructor parameters and request validation logic --- src/PriorityWithdrawalQueue.sol | 36 +++++++++++++++++------------- test/PriorityWithdrawalQueue.t.sol | 3 ++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 89075dfe4..4bc850568 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -32,8 +32,6 @@ contract PriorityWithdrawalQueue is //--------------------------------- CONSTANTS ---------------------------------------- //-------------------------------------------------------------------------------------- - /// @notice Minimum delay in seconds before a request can be fulfilled - uint32 public constant MIN_DELAY = 1 hours; /// @notice Minimum eETH amount per withdrawal request uint96 public constant MIN_AMOUNT = 0.01 ether; @@ -49,6 +47,8 @@ contract PriorityWithdrawalQueue is IeETH public immutable eETH; IRoleRegistry public immutable roleRegistry; address public immutable treasury; + /// @notice Minimum delay in seconds before a request can be fulfilled + uint32 public immutable MIN_DELAY; //-------------------------------------------------------------------------------------- //--------------------------------- STATE-VARIABLES ---------------------------------- @@ -163,7 +163,7 @@ contract PriorityWithdrawalQueue is //-------------------------------------------------------------------------------------- /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _liquidityPool, address _eETH, address _roleRegistry, address _treasury) { + constructor(address _liquidityPool, address _eETH, address _roleRegistry, address _treasury, uint32 _minDelay) { if (_liquidityPool == address(0) || _eETH == address(0) || _roleRegistry == address(0) || _treasury == address(0)) { revert AddressZero(); } @@ -172,6 +172,7 @@ contract PriorityWithdrawalQueue is eETH = IeETH(_eETH); roleRegistry = IRoleRegistry(_roleRegistry); treasury = _treasury; + MIN_DELAY = _minDelay; _disableInitializers(); } @@ -283,15 +284,16 @@ contract PriorityWithdrawalQueue is WithdrawRequest calldata request = requests[i]; bytes32 requestId = keccak256(abi.encode(request)); - // Verify request exists in pending set - if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + // Check finalized first for better error message (since finalized requests are removed from _withdrawRequests) if (_finalizedRequests.contains(requestId)) revert RequestAlreadyFinalized(); + if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); // Check MIN_DELAY has passed (request must wait at least MIN_DELAY seconds) uint256 earliestFulfillTime = request.creationTime + MIN_DELAY; if (block.timestamp < earliestFulfillTime) revert NotMatured(); - // Add to finalized set + // Move from pending to finalized set + _withdrawRequests.remove(requestId); _finalizedRequests.add(requestId); totalSharesToFinalize += request.shareOfEEth; @@ -344,7 +346,8 @@ contract PriorityWithdrawalQueue is invalidatedRequestIds = new bytes32[](requests.length); for (uint256 i = 0; i < requests.length; ++i) { bytes32 requestId = keccak256(abi.encode(requests[i])); - if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + // Check both sets since pending requests are in _withdrawRequests, finalized in _finalizedRequests + if (!_withdrawRequests.contains(requestId) && !_finalizedRequests.contains(requestId)) revert RequestNotFound(); _cancelWithdrawRequest(requests[i]); invalidatedRequestIds[i] = requestId; @@ -512,10 +515,14 @@ contract PriorityWithdrawalQueue is function _dequeueWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { requestId = keccak256(abi.encode(request)); - bool removedFromSet = _withdrawRequests.remove(requestId); - if (!removedFromSet) revert RequestNotFound(); - - _finalizedRequests.remove(requestId); + + // Try to remove from finalized set first (finalized requests are only in _finalizedRequests) + bool removedFromFinalized = _finalizedRequests.remove(requestId); + if (removedFromFinalized) return requestId; + + // If not finalized, try to remove from pending set + bool removedFromPending = _withdrawRequests.remove(requestId); + if (!removedFromPending) revert RequestNotFound(); } function _cancelWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { @@ -556,7 +563,7 @@ contract PriorityWithdrawalQueue is bytes32 requestId = keccak256(abi.encode(request)); - if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); + // Only check finalized set (finalized requests are removed from _withdrawRequests) if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); @@ -566,7 +573,6 @@ contract PriorityWithdrawalQueue is uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); - _withdrawRequests.remove(requestId); _finalizedRequests.remove(requestId); // Track remainder (difference between original shares and burned shares) @@ -638,11 +644,11 @@ contract PriorityWithdrawalQueue is return _finalizedRequests.values(); } - /// @notice Check if a request exists + /// @notice Check if a request exists (pending or finalized) /// @param requestId The request ID to check /// @return Whether the request exists function requestExists(bytes32 requestId) external view returns (bool) { - return _withdrawRequests.contains(requestId); + return _withdrawRequests.contains(requestId) || _finalizedRequests.contains(requestId); } /// @notice Check if a request is finalized diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index 3ff0ee39d..37ff27b91 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -37,7 +37,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { address(liquidityPoolInstance), address(eETHInstance), address(roleRegistryInstance), - treasury + treasury, + 1 hours ); UUPSProxy proxy = new UUPSProxy( address(priorityQueueImplementation), From d31e074865651217b9425b05f822725d1684851a Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Wed, 28 Jan 2026 17:14:06 -0500 Subject: [PATCH 09/19] refactor: Clean up PriorityWithdrawalQueue contract by removing redundant comments and simplifying logic - Removed unnecessary comments and documentation for clarity - Streamlined canceling withdrawal request handling by directly using request parameters - Enhanced event emissions to reflect accurate withdrawal amounts and shares --- src/PriorityWithdrawalQueue.sol | 81 ++------------------------------- 1 file changed, 3 insertions(+), 78 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 4bc850568..fd2c82524 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -32,11 +32,7 @@ contract PriorityWithdrawalQueue is //--------------------------------- CONSTANTS ---------------------------------------- //-------------------------------------------------------------------------------------- - - /// @notice Minimum eETH amount per withdrawal request uint96 public constant MIN_AMOUNT = 0.01 ether; - - /// @notice Basis point scale for fee calculations (100% = 10000) uint256 private constant _BASIS_POINT_SCALE = 1e4; //-------------------------------------------------------------------------------------- @@ -47,7 +43,6 @@ contract PriorityWithdrawalQueue is IeETH public immutable eETH; IRoleRegistry public immutable roleRegistry; address public immutable treasury; - /// @notice Minimum delay in seconds before a request can be fulfilled uint32 public immutable MIN_DELAY; //-------------------------------------------------------------------------------------- @@ -60,19 +55,11 @@ contract PriorityWithdrawalQueue is /// @notice Set of finalized request IDs (fulfilled and ready for claim) EnumerableSet.Bytes32Set private _finalizedRequests; - /// @notice Mapping of whitelisted addresses mapping(address => bool) public isWhitelisted; - /// @notice Request nonce to prevent hash collisions uint32 public nonce; - - /// @notice Fee split to treasury in basis points (e.g., 5000 = 50%) uint16 public shareRemainderSplitToTreasuryInBps; - - /// @notice Contract pause state bool public paused; - - /// @notice Remainder shares from claimed withdrawals (difference between request shares and actual burned) uint96 public totalRemainderShares; //-------------------------------------------------------------------------------------- @@ -177,7 +164,6 @@ contract PriorityWithdrawalQueue is _disableInitializers(); } - /// @notice Initialize the contract function initialize() external initializer { __Ownable_init(); __UUPSUpgradeable_init(); @@ -284,15 +270,12 @@ contract PriorityWithdrawalQueue is WithdrawRequest calldata request = requests[i]; bytes32 requestId = keccak256(abi.encode(request)); - // Check finalized first for better error message (since finalized requests are removed from _withdrawRequests) if (_finalizedRequests.contains(requestId)) revert RequestAlreadyFinalized(); if (!_withdrawRequests.contains(requestId)) revert RequestNotFound(); - // Check MIN_DELAY has passed (request must wait at least MIN_DELAY seconds) uint256 earliestFulfillTime = request.creationTime + MIN_DELAY; if (block.timestamp < earliestFulfillTime) revert NotMatured(); - // Move from pending to finalized set _withdrawRequests.remove(requestId); _finalizedRequests.add(requestId); totalSharesToFinalize += request.shareOfEEth; @@ -300,7 +283,6 @@ contract PriorityWithdrawalQueue is emit WithdrawRequestFinalized(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); } - // Lock ETH in LiquidityPool for priority withdrawals uint256 totalAmountToLock = liquidityPool.amountForShare(totalSharesToFinalize); liquidityPool.addEthAmountLockedForPriorityWithdrawal(uint128(totalAmountToLock)); } @@ -382,8 +364,6 @@ contract PriorityWithdrawalQueue is emit RemainderHandled(uint96(eEthAmountToTreasury), uint96(liquidityPool.amountForShare(eEthSharesToBurn))); } - /// @notice Update the share remainder split to treasury - /// @param _shareRemainderSplitToTreasuryInBps New split percentage in basis points (max 10000) function updateShareRemainderSplitToTreasury(uint16 _shareRemainderSplitToTreasuryInBps) external onlyAdmin { if (_shareRemainderSplitToTreasuryInBps > _BASIS_POINT_SCALE) revert BadInput(); shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; @@ -439,9 +419,7 @@ contract PriorityWithdrawalQueue is uint256 queueEEthSharesBefore, uint256 userEEthSharesBefore ) internal view { - // LP ETH should be unchanged (no ETH transferred in cancel) if (address(liquidityPool).balance != lpEthBefore) revert UnexpectedBalanceChange(); - // Queue should have less eETH shares and user should have more if (eETH.shares(address(this)) >= queueEEthSharesBefore) revert UnexpectedBalanceChange(); if (eETH.shares(msg.sender) <= userEEthSharesBefore) revert UnexpectedBalanceChange(); } @@ -457,11 +435,8 @@ contract PriorityWithdrawalQueue is uint256 userEthBefore, address user ) internal view { - // LP ETH should have decreased (user withdrew ETH) if (address(liquidityPool).balance >= lpEthBefore) revert UnexpectedBalanceChange(); - // Queue eETH shares should have decreased (shares burned) if (eETH.shares(address(this)) >= queueEEthSharesBefore) revert UnexpectedBalanceChange(); - // User ETH should have increased if (user.balance <= userEthBefore) revert UnexpectedBalanceChange(); } @@ -516,11 +491,9 @@ contract PriorityWithdrawalQueue is function _dequeueWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { requestId = keccak256(abi.encode(request)); - // Try to remove from finalized set first (finalized requests are only in _finalizedRequests) bool removedFromFinalized = _finalizedRequests.remove(requestId); if (removedFromFinalized) return requestId; - // If not finalized, try to remove from pending set bool removedFromPending = _withdrawRequests.remove(requestId); if (!removedFromPending) revert RequestNotFound(); } @@ -528,34 +501,17 @@ contract PriorityWithdrawalQueue is function _cancelWithdrawRequest(WithdrawRequest calldata request) internal returns (bytes32 requestId) { requestId = keccak256(abi.encode(request)); - // Check if finalized BEFORE dequeue (dequeue removes from finalized set) bool wasFinalized = _finalizedRequests.contains(requestId); _dequeueWithdrawRequest(request); - // Calculate current value of shares - uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); - uint256 amountToReturn = request.amountOfEEth < amountForShares - ? request.amountOfEEth - : amountForShares; - - // Calculate shares being transferred back - uint256 sharesToTransfer = liquidityPool.sharesForAmount(amountToReturn); - - // Track remainder (difference between original shares and transferred shares) - // This captures value from positive rebases where user gets original amount using fewer shares - uint256 remainder = request.shareOfEEth > sharesToTransfer - ? request.shareOfEEth - sharesToTransfer - : 0; - totalRemainderShares += uint96(remainder); - if (wasFinalized) { - liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToReturn)); + liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(request.amountOfEEth)); } - IERC20(address(eETH)).safeTransfer(request.user, amountToReturn); + IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); - emit WithdrawRequestCancelled(requestId, request.user, uint96(amountToReturn), uint96(sharesToTransfer), request.nonce, uint32(block.timestamp)); + emit WithdrawRequestCancelled(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); } function _claimWithdraw(WithdrawRequest calldata request) internal { @@ -563,7 +519,6 @@ contract PriorityWithdrawalQueue is bytes32 requestId = keccak256(abi.encode(request)); - // Only check finalized set (finalized requests are removed from _withdrawRequests) if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); @@ -575,7 +530,6 @@ contract PriorityWithdrawalQueue is _finalizedRequests.remove(requestId); - // Track remainder (difference between original shares and burned shares) uint256 remainder = request.shareOfEEth > sharesToBurn ? request.shareOfEEth - sharesToBurn : 0; @@ -595,13 +549,6 @@ contract PriorityWithdrawalQueue is //------------------------------------ GETTERS --------------------------------------- //-------------------------------------------------------------------------------------- - /// @notice Generate a request ID from individual parameters - /// @param _user The user address - /// @param _amountOfEEth The amount of eETH - /// @param _shareOfEEth The share of eETH - /// @param _nonce The request nonce - /// @param _creationTime The creation timestamp - /// @return requestId The keccak256 hash of the request function generateWithdrawRequestId( address _user, uint96 _amountOfEEth, @@ -619,9 +566,6 @@ contract PriorityWithdrawalQueue is requestId = keccak256(abi.encode(req)); } - /// @notice Get the request ID from a request struct - /// @param request The withdrawal request - /// @return requestId The keccak256 hash of the request function getRequestId(WithdrawRequest calldata request) external pure returns (bytes32) { return generateWithdrawRequestId( request.user, @@ -632,35 +576,22 @@ contract PriorityWithdrawalQueue is ); } - /// @notice Get all active request IDs - /// @return Array of request IDs function getRequestIds() external view returns (bytes32[] memory) { return _withdrawRequests.values(); } - /// @notice Get all finalized request IDs - /// @return Array of finalized request IDs function getFinalizedRequestIds() external view returns (bytes32[] memory) { return _finalizedRequests.values(); } - /// @notice Check if a request exists (pending or finalized) - /// @param requestId The request ID to check - /// @return Whether the request exists function requestExists(bytes32 requestId) external view returns (bool) { return _withdrawRequests.contains(requestId) || _finalizedRequests.contains(requestId); } - /// @notice Check if a request is finalized - /// @param requestId The request ID to check - /// @return Whether the request is finalized function isFinalized(bytes32 requestId) external view returns (bool) { return _finalizedRequests.contains(requestId); } - /// @notice Get the claimable amount for a request - /// @param request The withdrawal request - /// @return The claimable ETH amount function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256) { bytes32 requestId = keccak256(abi.encode(request)); if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); @@ -669,20 +600,14 @@ contract PriorityWithdrawalQueue is return request.amountOfEEth < amountForShares ? request.amountOfEEth : amountForShares; } - /// @notice Get the total number of active requests - /// @return The number of active requests function totalActiveRequests() external view returns (uint256) { return _withdrawRequests.length(); } - /// @notice Get the total remainder amount available - /// @return The total remainder eETH amount function getRemainderAmount() external view returns (uint256) { return liquidityPool.amountForShare(totalRemainderShares); } - /// @notice Get the implementation address - /// @return The implementation address function getImplementation() external view returns (address) { return _getImplementation(); } From 1152b54370bcc6cbd8b790dbb20a602487db4666 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 12:11:09 -0500 Subject: [PATCH 10/19] refactor: Updated claimWithdraw and batchClaimWithdraw functions to allow any user to claim on behalf of the request user - Updated handling of canceling of finalized requests --- src/PriorityWithdrawalQueue.sol | 16 +++++++++++----- test/PriorityWithdrawalQueue.t.sol | 21 ++------------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index fd2c82524..074b04b0e 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -228,6 +228,7 @@ contract PriorityWithdrawalQueue is } /// @notice Claim ETH for a finalized withdrawal request + /// @dev Anyone can call this to claim on behalf of the user. Funds are sent to request.user. /// @param request The withdrawal request to claim function claimWithdraw(WithdrawRequest calldata request) external whenNotPaused nonReentrant { if (request.creationTime + MIN_DELAY > block.timestamp) revert NotMatured(); @@ -241,6 +242,7 @@ contract PriorityWithdrawalQueue is } /// @notice Batch claim multiple withdrawal requests + /// @dev Anyone can call this to claim on behalf of users. Funds are sent to each request.user. /// @param requests Array of withdrawal requests to claim function batchClaimWithdraw(WithdrawRequest[] calldata requests) external whenNotPaused nonReentrant { (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); @@ -506,7 +508,11 @@ contract PriorityWithdrawalQueue is _dequeueWithdrawRequest(request); if (wasFinalized) { - liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(request.amountOfEEth)); + uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); + uint256 amountToUnlock = request.amountOfEEth < amountForShares + ? request.amountOfEEth + : amountForShares; + liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToUnlock)); } IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); @@ -514,9 +520,9 @@ contract PriorityWithdrawalQueue is emit WithdrawRequestCancelled(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); } + /// @dev Claims a finalized withdrawal request. Anyone can call to claim on behalf of the user. + /// @param request The withdrawal request to claim function _claimWithdraw(WithdrawRequest calldata request) internal { - if (request.user != msg.sender) revert NotRequestOwner(); - bytes32 requestId = keccak256(abi.encode(request)); if (!_finalizedRequests.contains(requestId)) revert RequestNotFinalized(); @@ -535,10 +541,10 @@ contract PriorityWithdrawalQueue is : 0; totalRemainderShares += uint96(remainder); - uint256 burnedShares = liquidityPool.withdraw(msg.sender, amountToWithdraw); + uint256 burnedShares = liquidityPool.withdraw(request.user, amountToWithdraw); if (burnedShares != sharesToBurn) revert InvalidBurnedSharesAmount(); - emit WithdrawRequestClaimed(requestId, msg.sender, uint96(amountToWithdraw), uint96(sharesToBurn), request.nonce, uint32(block.timestamp)); + emit WithdrawRequestClaimed(requestId, request.user, uint96(amountToWithdraw), uint96(sharesToBurn), request.nonce, uint32(block.timestamp)); } function _authorizeUpgrade(address) internal override { diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index 37ff27b91..2c7cbba99 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -273,8 +273,8 @@ contract PriorityWithdrawalQueueTest is TestSetup { uint256 queueEethBefore = eETHInstance.balanceOf(address(priorityQueue)); uint256 remainderBefore = priorityQueue.totalRemainderShares(); - // VIP user claims their ETH - vm.prank(vipUser); + // Anyone can send the ETH to the request user + vm.prank(regularUser); priorityQueue.claimWithdraw(request); // Verify ETH was received (approximately, due to share price) @@ -783,23 +783,6 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.claimWithdraw(request); } - function test_revert_claimWrongOwner() public { - // VIP creates request - (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = - _createWithdrawRequest(vipUser, 1 ether); - - // Fulfill - IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); - requests[0] = request; - vm.prank(requestManager); - priorityQueue.fulfillRequests(requests); - - // Another user tries to claim - vm.prank(regularUser); - vm.expectRevert(PriorityWithdrawalQueue.NotRequestOwner.selector); - priorityQueue.claimWithdraw(request); - } - function test_revert_fulfillNonRequestManager() public { (, IPriorityWithdrawalQueue.WithdrawRequest memory request) = _createWithdrawRequest(vipUser, 1 ether); From 365b62e8d2fb8dd605a2e26ce7f3bd286e4b0b73 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 13:28:26 -0500 Subject: [PATCH 11/19] refactor: Improve cancellation logic in PriorityWithdrawalQueue contract - Updated user reference in cancel withdrawal request handling to use request.user instead of msg.sender - Simplified request validation by using logical OR in the RequestNotFound check - Enhanced post-condition verification to include the user who cancelled the request --- src/PriorityWithdrawalQueue.sol | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 074b04b0e..6b450c2c9 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -220,11 +220,11 @@ contract PriorityWithdrawalQueue is ) external whenNotPaused onlyRequestUser(request.user) nonReentrant returns (bytes32 requestId) { if (request.creationTime + MIN_DELAY > block.timestamp) revert NotMatured(); (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); - uint256 userEEthSharesBefore = eETH.shares(msg.sender); + uint256 userEEthSharesBefore = eETH.shares(request.user); requestId = _cancelWithdrawRequest(request); - _verifyCancelPostConditions(lpEthBefore, queueEEthSharesBefore, userEEthSharesBefore); + _verifyCancelPostConditions(lpEthBefore, queueEEthSharesBefore, userEEthSharesBefore, request.user); } /// @notice Claim ETH for a finalized withdrawal request @@ -331,7 +331,7 @@ contract PriorityWithdrawalQueue is for (uint256 i = 0; i < requests.length; ++i) { bytes32 requestId = keccak256(abi.encode(requests[i])); // Check both sets since pending requests are in _withdrawRequests, finalized in _finalizedRequests - if (!_withdrawRequests.contains(requestId) && !_finalizedRequests.contains(requestId)) revert RequestNotFound(); + if (!(_withdrawRequests.contains(requestId) || _finalizedRequests.contains(requestId))) revert RequestNotFound(); _cancelWithdrawRequest(requests[i]); invalidatedRequestIds[i] = requestId; @@ -416,14 +416,16 @@ contract PriorityWithdrawalQueue is /// @param lpEthBefore ETH balance of LiquidityPool before operation /// @param queueEEthSharesBefore eETH shares held by queue before operation /// @param userEEthSharesBefore eETH shares held by user before operation + /// @param user The user who cancelled function _verifyCancelPostConditions( uint256 lpEthBefore, uint256 queueEEthSharesBefore, - uint256 userEEthSharesBefore + uint256 userEEthSharesBefore, + address user ) internal view { if (address(liquidityPool).balance != lpEthBefore) revert UnexpectedBalanceChange(); if (eETH.shares(address(this)) >= queueEEthSharesBefore) revert UnexpectedBalanceChange(); - if (eETH.shares(msg.sender) <= userEEthSharesBefore) revert UnexpectedBalanceChange(); + if (eETH.shares(user) <= userEEthSharesBefore) revert UnexpectedBalanceChange(); } /// @dev Verify post-conditions after a claim operation @@ -457,10 +459,7 @@ contract PriorityWithdrawalQueue is address user, uint96 amountOfEEth ) internal returns (bytes32 requestId, WithdrawRequest memory req) { - uint32 requestNonce; - unchecked { - requestNonce = uint32(nonce++); - } + uint32 requestNonce = nonce++; uint96 shareOfEEth = uint96(liquidityPool.sharesForAmount(amountOfEEth)); if (shareOfEEth == 0) revert InvalidAmount(); From 1c07a431d2b60d5935f62fddb8e55b8d5d8e26d2 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 13:46:35 -0500 Subject: [PATCH 12/19] feat: Add minAmountOut parameter for withdrawal requests - Introduced minAmountOut parameter to requestWithdraw and requestWithdrawWithPermit functions for slippage protection. - Updated WithdrawRequest struct to include minAmountOut for better tracking of withdrawal conditions. - Enhanced error handling to revert transactions if the output amount is insufficient. - Modified tests to cover new functionality and ensure proper behavior with minAmountOut. --- src/PriorityWithdrawalQueue.sol | 22 ++++++++-- src/interfaces/IPriorityWithdrawalQueue.sol | 10 +++-- test/PriorityWithdrawalQueue.t.sol | 45 ++++++++++++++++++--- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 6b450c2c9..b5f8a9b62 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -82,6 +82,7 @@ contract PriorityWithdrawalQueue is address indexed user, uint96 amountOfEEth, uint96 shareOfEEth, + uint96 minAmountOut, uint32 nonce, uint32 creationTime ); @@ -115,6 +116,7 @@ contract PriorityWithdrawalQueue is error BadInput(); error InvalidBurnedSharesAmount(); error InvalidEEthSharesAfterRemainderHandling(); + error InsufficientOutputAmount(); //-------------------------------------------------------------------------------------- //----------------------------------- MODIFIERS -------------------------------------- @@ -179,25 +181,29 @@ contract PriorityWithdrawalQueue is /// @notice Request a withdrawal of eETH /// @param amountOfEEth Amount of eETH to withdraw + /// @param minAmountOut Minimum ETH output amount (slippage protection for dynamic fees) /// @return requestId The hash-based ID of the created withdrawal request function requestWithdraw( - uint96 amountOfEEth + uint96 amountOfEEth, + uint96 minAmountOut ) external whenNotPaused onlyWhitelisted nonReentrant returns (bytes32 requestId) { if (amountOfEEth < MIN_AMOUNT) revert InvalidAmount(); (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); - (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth); + (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth, minAmountOut); _verifyRequestPostConditions(lpEthBefore, queueEEthSharesBefore, amountOfEEth); } /// @notice Request a withdrawal with permit for gasless approval /// @param amountOfEEth Amount of eETH to withdraw + /// @param minAmountOut Minimum ETH output amount (slippage protection for dynamic fees) /// @param permit Permit signature data for eETH approval /// @return requestId The hash-based ID of the created withdrawal request function requestWithdrawWithPermit( uint96 amountOfEEth, + uint96 minAmountOut, PermitInput calldata permit ) external whenNotPaused onlyWhitelisted nonReentrant returns (bytes32 requestId) { if (amountOfEEth < MIN_AMOUNT) revert InvalidAmount(); @@ -207,7 +213,7 @@ contract PriorityWithdrawalQueue is IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); - (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth); + (requestId,) = _queueWithdrawRequest(msg.sender, amountOfEEth, minAmountOut); _verifyRequestPostConditions(lpEthBefore, queueEEthSharesBefore, amountOfEEth); } @@ -457,7 +463,8 @@ contract PriorityWithdrawalQueue is function _queueWithdrawRequest( address user, - uint96 amountOfEEth + uint96 amountOfEEth, + uint96 minAmountOut ) internal returns (bytes32 requestId, WithdrawRequest memory req) { uint32 requestNonce = nonce++; @@ -470,6 +477,7 @@ contract PriorityWithdrawalQueue is user: user, amountOfEEth: amountOfEEth, shareOfEEth: shareOfEEth, + minAmountOut: minAmountOut, nonce: requestNonce, creationTime: timeNow }); @@ -484,6 +492,7 @@ contract PriorityWithdrawalQueue is user, amountOfEEth, shareOfEEth, + minAmountOut, requestNonce, timeNow ); @@ -531,6 +540,8 @@ contract PriorityWithdrawalQueue is ? request.amountOfEEth : amountForShares; + if (amountToWithdraw < request.minAmountOut) revert InsufficientOutputAmount(); + uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); _finalizedRequests.remove(requestId); @@ -558,6 +569,7 @@ contract PriorityWithdrawalQueue is address _user, uint96 _amountOfEEth, uint96 _shareOfEEth, + uint96 _minAmountOut, uint32 _nonce, uint32 _creationTime ) public pure returns (bytes32 requestId) { @@ -565,6 +577,7 @@ contract PriorityWithdrawalQueue is user: _user, amountOfEEth: _amountOfEEth, shareOfEEth: _shareOfEEth, + minAmountOut: _minAmountOut, nonce: _nonce, creationTime: _creationTime }); @@ -576,6 +589,7 @@ contract PriorityWithdrawalQueue is request.user, request.amountOfEEth, request.shareOfEEth, + request.minAmountOut, request.nonce, request.creationTime ); diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol index e73bd6cf2..6205b8c47 100644 --- a/src/interfaces/IPriorityWithdrawalQueue.sol +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -6,14 +6,16 @@ interface IPriorityWithdrawalQueue { /// @param user The user who created the request /// @param amountOfEEth Original eETH amount requested /// @param shareOfEEth eETH shares at time of request + /// @param minAmountOut Minimum ETH output amount (slippage protection for dynamic fees) /// @param nonce Unique nonce to prevent hash collisions /// @param creationTime Timestamp when request was created struct WithdrawRequest { address user; // 20 bytes uint96 amountOfEEth; // 12 bytes | Slot 1 = 32 bytes uint96 shareOfEEth; // 12 bytes + uint96 minAmountOut; // 12 bytes uint32 nonce; // 4 bytes - uint32 creationTime; // 4 bytes | Slot 2 = 20 bytes + uint32 creationTime; // 4 bytes | Slot 2 = 32 bytes } struct PermitInput { @@ -25,8 +27,8 @@ interface IPriorityWithdrawalQueue { } // User functions - function requestWithdraw(uint96 amountOfEEth) external returns (bytes32 requestId); - function requestWithdrawWithPermit(uint96 amountOfEEth, PermitInput calldata permit) external returns (bytes32 requestId); + function requestWithdraw(uint96 amountOfEEth, uint96 minAmountOut) external returns (bytes32 requestId); + function requestWithdrawWithPermit(uint96 amountOfEEth, uint96 minAmountOut, PermitInput calldata permit) external returns (bytes32 requestId); function cancelWithdraw(WithdrawRequest calldata request) external returns (bytes32 requestId); function claimWithdraw(WithdrawRequest calldata request) external; function batchClaimWithdraw(WithdrawRequest[] calldata requests) external; @@ -37,6 +39,7 @@ interface IPriorityWithdrawalQueue { function getClaimableAmount(WithdrawRequest calldata request) external view returns (uint256); function isWhitelisted(address user) external view returns (bool); function nonce() external view returns (uint32); + function shareRemainderSplitToTreasuryInBps() external view returns (uint16); // Constants function MIN_DELAY() external view returns (uint32); @@ -57,5 +60,4 @@ interface IPriorityWithdrawalQueue { // Immutables function treasury() external view returns (address); - function shareRemainderSplitToTreasuryInBps() external view returns (uint16); } diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index 2c7cbba99..83835a682 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -84,6 +84,14 @@ contract PriorityWithdrawalQueueTest is TestSetup { function _createWithdrawRequest(address user, uint96 amount) internal returns (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) + { + return _createWithdrawRequestWithMinOut(user, amount, 0); + } + + /// @dev Helper to create a withdrawal request with custom minAmountOut + function _createWithdrawRequestWithMinOut(address user, uint96 amount, uint96 minAmountOut) + internal + returns (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) { uint32 nonceBefore = priorityQueue.nonce(); uint96 shareAmount = uint96(liquidityPoolInstance.sharesForAmount(amount)); @@ -91,7 +99,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.startPrank(user); eETHInstance.approve(address(priorityQueue), amount); - requestId = priorityQueue.requestWithdraw(amount); + requestId = priorityQueue.requestWithdraw(amount, minAmountOut); vm.stopPrank(); // Reconstruct the request struct @@ -99,6 +107,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { user: user, amountOfEEth: amount, shareOfEEth: shareAmount, + minAmountOut: minAmountOut, nonce: uint32(nonceBefore), creationTime: timestamp }); @@ -204,13 +213,14 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.startPrank(vipUser); eETHInstance.approve(address(priorityQueue), withdrawAmount); - priorityQueue.requestWithdraw(withdrawAmount); + priorityQueue.requestWithdraw(withdrawAmount, 0); vm.stopPrank(); IPriorityWithdrawalQueue.WithdrawRequest memory request = IPriorityWithdrawalQueue.WithdrawRequest({ user: vipUser, amountOfEEth: withdrawAmount, shareOfEEth: shareAmount, + minAmountOut: 0, nonce: uint32(nonceBefore), creationTime: timestamp }); @@ -549,7 +559,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.startPrank(vipUser); eETHInstance.approve(address(priorityQueue), 1 ether); vm.expectRevert(PriorityWithdrawalQueue.ContractPaused.selector); - priorityQueue.requestWithdraw(1 ether); + priorityQueue.requestWithdraw(1 ether, 0); vm.stopPrank(); } @@ -769,7 +779,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { eETHInstance.approve(address(priorityQueue), 1 ether); vm.expectRevert(PriorityWithdrawalQueue.NotWhitelisted.selector); - priorityQueue.requestWithdraw(1 ether); + priorityQueue.requestWithdraw(1 ether, 0); vm.stopPrank(); } @@ -810,7 +820,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Default minimum amount is 0.01 ether vm.expectRevert(PriorityWithdrawalQueue.InvalidAmount.selector); - priorityQueue.requestWithdraw(0.001 ether); + priorityQueue.requestWithdraw(0.001 ether, 0); vm.stopPrank(); } @@ -820,6 +830,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { user: vipUser, amountOfEEth: 1 ether, shareOfEEth: 1 ether, + minAmountOut: 0, nonce: 999, creationTime: uint32(block.timestamp) }); @@ -868,6 +879,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { address testUser = vipUser; uint96 testAmount = 10 ether; uint96 testShare = uint96(liquidityPoolInstance.sharesForAmount(testAmount)); + uint96 testMinOut = 9.5 ether; uint32 testNonce = 1; uint32 testTime = uint32(block.timestamp); @@ -875,6 +887,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { testUser, testAmount, testShare, + testMinOut, testNonce, testTime ); @@ -884,6 +897,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { user: testUser, amountOfEEth: testAmount, shareOfEEth: testShare, + minAmountOut: testMinOut, nonce: testNonce, creationTime: testTime }); @@ -892,4 +906,25 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertEq(generatedId, expectedId, "Generated ID should match"); } + function test_revert_insufficientOutputAmount() public { + // User requests with a high minAmountOut that won't be met after fees + uint96 withdrawAmount = 1 ether; + uint96 highMinOut = 1.1 ether; // Higher than possible output + + (bytes32 requestId, IPriorityWithdrawalQueue.WithdrawRequest memory request) = + _createWithdrawRequestWithMinOut(vipUser, withdrawAmount, highMinOut); + + // Fulfill the request + IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); + requests[0] = request; + + vm.prank(requestManager); + priorityQueue.fulfillRequests(requests); + + // Claim should revert due to insufficient output + vm.prank(vipUser); + vm.expectRevert(PriorityWithdrawalQueue.InsufficientOutputAmount.selector); + priorityQueue.claimWithdraw(request); + } + } From 89c23e72d602983e80cf2b95c8c98920f36afa7f Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 14:22:06 -0500 Subject: [PATCH 13/19] refactor: Eliminated ethAmountLockedForPriorityWithdrawal and related functions from LiquidityPool to PriorityWithdrawalQueue --- src/LiquidityPool.sol | 22 --------------------- src/PriorityWithdrawalQueue.sol | 17 ++++++++-------- src/interfaces/ILiquidityPool.sol | 4 ---- src/interfaces/IPriorityWithdrawalQueue.sol | 1 + 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 13c06ac9e..a2ffb7d55 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -69,9 +69,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL IRoleRegistry public roleRegistry; uint256 public validatorSizeWei; - uint128 public ethAmountLockedForPriorityWithdrawal; - - //-------------------------------------------------------------------------------------- //------------------------------------- IMMUTABLES ---------------------------------- //-------------------------------------------------------------------------------------- @@ -225,8 +222,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL // Check liquidity based on caller if (msg.sender == address(withdrawRequestNFT)) { if (totalValueInLp < _amount || ethAmountLockedForWithdrawal < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); - } else if (msg.sender == priorityWithdrawalQueue) { - if (totalValueInLp < _amount || ethAmountLockedForPriorityWithdrawal < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); } else { if (totalValueInLp < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); } @@ -236,8 +231,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL totalValueInLp -= uint128(_amount); if (msg.sender == address(withdrawRequestNFT)) { ethAmountLockedForWithdrawal -= uint128(_amount); - } else if (msg.sender == priorityWithdrawalQueue) { - ethAmountLockedForPriorityWithdrawal -= uint128(_amount); } eETH.burnShares(msg.sender, share); @@ -502,21 +495,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL ethAmountLockedForWithdrawal += _amount; } - /// @notice Add ETH amount locked for priority withdrawal - /// @param _amount Amount of ETH to lock - function addEthAmountLockedForPriorityWithdrawal(uint128 _amount) external { - if (msg.sender != priorityWithdrawalQueue) revert IncorrectCaller(); - ethAmountLockedForPriorityWithdrawal += _amount; - } - - /// @notice Reduce ETH amount locked for priority withdrawal - /// @dev Can be called by priorityWithdrawalQueue (for canceling requests) - /// @param _amount Amount of ETH to unlock - function reduceEthAmountLockedForPriorityWithdrawal(uint128 _amount) external { - if (msg.sender != priorityWithdrawalQueue) revert IncorrectCaller(); - ethAmountLockedForPriorityWithdrawal -= _amount; - } - function burnEEthShares(uint256 shares) external { if (msg.sender != address(etherFiRedemptionManager) && msg.sender != address(withdrawRequestNFT) && msg.sender != priorityWithdrawalQueue) revert IncorrectCaller(); eETH.burnShares(msg.sender, shares); diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index b5f8a9b62..f7958a327 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -61,6 +61,7 @@ contract PriorityWithdrawalQueue is uint16 public shareRemainderSplitToTreasuryInBps; bool public paused; uint96 public totalRemainderShares; + uint128 public ethAmountLockedForWithdrawal; //-------------------------------------------------------------------------------------- //------------------------------------- ROLES ---------------------------------------- @@ -292,7 +293,7 @@ contract PriorityWithdrawalQueue is } uint256 totalAmountToLock = liquidityPool.amountForShare(totalSharesToFinalize); - liquidityPool.addEthAmountLockedForPriorityWithdrawal(uint128(totalAmountToLock)); + ethAmountLockedForWithdrawal += uint128(totalAmountToLock); } //-------------------------------------------------------------------------------------- @@ -516,11 +517,11 @@ contract PriorityWithdrawalQueue is _dequeueWithdrawRequest(request); if (wasFinalized) { - uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); - uint256 amountToUnlock = request.amountOfEEth < amountForShares - ? request.amountOfEEth - : amountForShares; - liquidityPool.reduceEthAmountLockedForPriorityWithdrawal(uint128(amountToUnlock)); + uint256 amountForShares = liquidityPool.amountForShare(request.shareOfEEth); + uint256 amountToUnlock = request.amountOfEEth < amountForShares + ? request.amountOfEEth + : amountForShares; + ethAmountLockedForWithdrawal -= uint128(amountToUnlock); } IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); @@ -528,8 +529,6 @@ contract PriorityWithdrawalQueue is emit WithdrawRequestCancelled(requestId, request.user, request.amountOfEEth, request.shareOfEEth, request.nonce, uint32(block.timestamp)); } - /// @dev Claims a finalized withdrawal request. Anyone can call to claim on behalf of the user. - /// @param request The withdrawal request to claim function _claimWithdraw(WithdrawRequest calldata request) internal { bytes32 requestId = keccak256(abi.encode(request)); @@ -551,6 +550,8 @@ contract PriorityWithdrawalQueue is : 0; totalRemainderShares += uint96(remainder); + ethAmountLockedForWithdrawal -= uint128(amountToWithdraw); + uint256 burnedShares = liquidityPool.withdraw(request.user, amountToWithdraw); if (burnedShares != sharesToBurn) revert InvalidBurnedSharesAmount(); diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 2a11595ce..9741a2bf5 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -51,8 +51,6 @@ interface ILiquidityPool { function amountForShare(uint256 _share) external view returns (uint256); function eETH() external view returns (IeETH); function ethAmountLockedForWithdrawal() external view returns (uint128); - function priorityWithdrawalQueue() external view returns (address); - function ethAmountLockedForPriorityWithdrawal() external view returns (uint128); function deposit() external payable returns (uint256); function deposit(address _referral) external payable returns (uint256); @@ -76,8 +74,6 @@ interface ILiquidityPool { function rebase(int128 _accruedRewards) external; function payProtocolFees(uint128 _protocolFees) external; function addEthAmountLockedForWithdrawal(uint128 _amount) external; - function addEthAmountLockedForPriorityWithdrawal(uint128 _amount) external; - function reduceEthAmountLockedForPriorityWithdrawal(uint128 _amount) external; function pauseContract() external; function burnEEthShares(uint256 shares) external; diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol index 6205b8c47..f5f1855db 100644 --- a/src/interfaces/IPriorityWithdrawalQueue.sol +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -40,6 +40,7 @@ interface IPriorityWithdrawalQueue { function isWhitelisted(address user) external view returns (bool); function nonce() external view returns (uint32); function shareRemainderSplitToTreasuryInBps() external view returns (uint16); + function ethAmountLockedForWithdrawal() external view returns (uint128); // Constants function MIN_DELAY() external view returns (uint32); From 30d87da4b0649ef4e05109eb46a97eca310e5341 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 14:30:29 -0500 Subject: [PATCH 14/19] revert: liquidityPool updated checks --- src/LiquidityPool.sol | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index a2ffb7d55..412c4298b 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -218,14 +218,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL msg.sender == priorityWithdrawalQueue, "Incorrect Caller" ); - - // Check liquidity based on caller - if (msg.sender == address(withdrawRequestNFT)) { - if (totalValueInLp < _amount || ethAmountLockedForWithdrawal < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); - } else { - if (totalValueInLp < _amount || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); - } - + if (totalValueInLp < _amount || (msg.sender == address(withdrawRequestNFT) && ethAmountLockedForWithdrawal < _amount) || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); if (_amount > type(uint128).max || _amount == 0 || share == 0) revert InvalidAmount(); totalValueInLp -= uint128(_amount); From 307698f33c4d541c0f509b3f92ea47734181dada Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 14:45:51 -0500 Subject: [PATCH 15/19] feat: Integrate PriorityWithdrawalQueue into EtherFiRedemptionManager - Modified getInstantLiquidityAmount function to account for ethAmountLockedForPriorityWithdrawal. - Refactored PriorityWithdrawalQueue to rename ethAmountLockedForWithdrawal to ethAmountLockedForPriorityWithdrawal, ensuring consistency across contracts. - Updated tests to reflect changes in the PriorityWithdrawalQueue implementation and its interactions. --- src/EtherFiRedemptionManager.sol | 7 +++- src/PriorityWithdrawalQueue.sol | 8 ++-- src/interfaces/IPriorityWithdrawalQueue.sol | 2 +- test/PriorityWithdrawalQueue.t.sol | 16 ++++---- test/TestSetup.sol | 44 +++++---------------- 5 files changed, 27 insertions(+), 50 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 0c1a37935..690b6128e 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -22,6 +22,7 @@ import "./EtherFiRestaker.sol"; import "lib/BucketLimiter.sol"; import "./RoleRegistry.sol"; +import "./interfaces/IPriorityWithdrawalQueue.sol"; /* The contract allows instant redemption of eETH and weETH tokens to ETH or stETH with an exit fee. @@ -53,6 +54,7 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra ILiquidityPool public immutable liquidityPool; EtherFiRestaker public immutable etherFiRestaker; ILido public immutable lido; + IPriorityWithdrawalQueue public immutable priorityWithdrawalQueue; mapping(address => RedemptionInfo) public tokenToRedemptionInfo; @@ -66,7 +68,7 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry, address _etherFiRestaker) { + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry, address _etherFiRestaker, address _priorityWithdrawalQueue) { roleRegistry = RoleRegistry(_roleRegistry); treasury = _treasury; liquidityPool = ILiquidityPool(payable(_liquidityPool)); @@ -74,6 +76,7 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra weEth = IWeETH(_weEth); etherFiRestaker = EtherFiRestaker(payable(_etherFiRestaker)); lido = etherFiRestaker.lido(); + priorityWithdrawalQueue = IPriorityWithdrawalQueue(_priorityWithdrawalQueue); _disableInitializers(); } @@ -241,7 +244,7 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra function getInstantLiquidityAmount(address token) public view returns (uint256) { if(token == ETH_ADDRESS) { - return address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + return address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal() - priorityWithdrawalQueue.ethAmountLockedForPriorityWithdrawal(); } else if (token == address(lido)) { return lido.balanceOf(address(etherFiRestaker)); } diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index f7958a327..398528090 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -61,7 +61,7 @@ contract PriorityWithdrawalQueue is uint16 public shareRemainderSplitToTreasuryInBps; bool public paused; uint96 public totalRemainderShares; - uint128 public ethAmountLockedForWithdrawal; + uint128 public ethAmountLockedForPriorityWithdrawal; //-------------------------------------------------------------------------------------- //------------------------------------- ROLES ---------------------------------------- @@ -293,7 +293,7 @@ contract PriorityWithdrawalQueue is } uint256 totalAmountToLock = liquidityPool.amountForShare(totalSharesToFinalize); - ethAmountLockedForWithdrawal += uint128(totalAmountToLock); + ethAmountLockedForPriorityWithdrawal += uint128(totalAmountToLock); } //-------------------------------------------------------------------------------------- @@ -521,7 +521,7 @@ contract PriorityWithdrawalQueue is uint256 amountToUnlock = request.amountOfEEth < amountForShares ? request.amountOfEEth : amountForShares; - ethAmountLockedForWithdrawal -= uint128(amountToUnlock); + ethAmountLockedForPriorityWithdrawal -= uint128(amountToUnlock); } IERC20(address(eETH)).safeTransfer(request.user, request.amountOfEEth); @@ -550,7 +550,7 @@ contract PriorityWithdrawalQueue is : 0; totalRemainderShares += uint96(remainder); - ethAmountLockedForWithdrawal -= uint128(amountToWithdraw); + ethAmountLockedForPriorityWithdrawal -= uint128(amountToWithdraw); uint256 burnedShares = liquidityPool.withdraw(request.user, amountToWithdraw); if (burnedShares != sharesToBurn) revert InvalidBurnedSharesAmount(); diff --git a/src/interfaces/IPriorityWithdrawalQueue.sol b/src/interfaces/IPriorityWithdrawalQueue.sol index f5f1855db..19255a523 100644 --- a/src/interfaces/IPriorityWithdrawalQueue.sol +++ b/src/interfaces/IPriorityWithdrawalQueue.sol @@ -40,7 +40,7 @@ interface IPriorityWithdrawalQueue { function isWhitelisted(address user) external view returns (bool); function nonce() external view returns (uint32); function shareRemainderSplitToTreasuryInBps() external view returns (uint16); - function ethAmountLockedForWithdrawal() external view returns (uint128); + function ethAmountLockedForPriorityWithdrawal() external view returns (uint128); // Constants function MIN_DELAY() external view returns (uint32); diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index 83835a682..f186ce9f6 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -9,7 +9,7 @@ import "../src/interfaces/IPriorityWithdrawalQueue.sol"; contract PriorityWithdrawalQueueTest is TestSetup { PriorityWithdrawalQueue public priorityQueue; - PriorityWithdrawalQueue public priorityQueueImplementation; + PriorityWithdrawalQueue public priorityQueueImpl; address public requestManager; address public vipUser; @@ -33,7 +33,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Deploy PriorityWithdrawalQueue with constructor args vm.startPrank(owner); - priorityQueueImplementation = new PriorityWithdrawalQueue( + priorityQueueImpl = new PriorityWithdrawalQueue( address(liquidityPoolInstance), address(eETHInstance), address(roleRegistryInstance), @@ -41,7 +41,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { 1 hours ); UUPSProxy proxy = new UUPSProxy( - address(priorityQueueImplementation), + address(priorityQueueImpl), abi.encodeWithSelector(PriorityWithdrawalQueue.initialize.selector) ); priorityQueue = PriorityWithdrawalQueue(address(proxy)); @@ -186,7 +186,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { _createWithdrawRequest(vipUser, withdrawAmount); // Record state before fulfillment - uint128 lpLockedBefore = liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(); + uint128 lpLockedBefore = priorityQueue.ethAmountLockedForPriorityWithdrawal(); // Request manager fulfills the request IPriorityWithdrawalQueue.WithdrawRequest[] memory requests = new IPriorityWithdrawalQueue.WithdrawRequest[](1); @@ -196,7 +196,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { priorityQueue.fulfillRequests(requests); // Verify state changes - assertGt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked for priority should increase"); + assertGt(priorityQueue.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked for priority should increase"); // Verify request is finalized assertTrue(priorityQueue.isFinalized(requestId), "Request should be finalized"); @@ -373,7 +373,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { vm.prank(requestManager); priorityQueue.fulfillRequests(requests); - uint128 lpLockedBefore = liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(); + uint128 lpLockedBefore = priorityQueue.ethAmountLockedForPriorityWithdrawal(); // Request manager cancels finalized request (invalidateRequests requires request manager role) vm.prank(requestManager); @@ -388,7 +388,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertApproxEqAbs(eETHInstance.balanceOf(vipUser), eethInitial, 1, "eETH should be returned"); // LP locked should decrease - assertLt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked should decrease"); + assertLt(priorityQueue.ethAmountLockedForPriorityWithdrawal(), lpLockedBefore, "LP locked should decrease"); } function test_admininvalidateRequests() public { @@ -442,7 +442,7 @@ contract PriorityWithdrawalQueueTest is TestSetup { // Verify fulfilled state assertTrue(priorityQueue.isFinalized(priorityQueue.getRequestId(request)), "Request should be finalized"); - assertGt(liquidityPoolInstance.ethAmountLockedForPriorityWithdrawal(), 0, "LP tracks locked amount"); + assertGt(priorityQueue.ethAmountLockedForPriorityWithdrawal(), 0, "LP tracks locked amount"); // 4. VIP user claims ETH vm.prank(vipUser); diff --git a/test/TestSetup.sol b/test/TestSetup.sol index cf027c167..8f4f3439f 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -61,6 +61,7 @@ import "../script/deploys/Deployed.s.sol"; import "../src/DepositAdapter.sol"; import "../src/interfaces/IWeETHWithdrawAdapter.sol"; +import "../src/PriorityWithdrawalQueue.sol"; contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { @@ -205,6 +206,9 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { IWeETHWithdrawAdapter public weEthWithdrawAdapterInstance; IWeETHWithdrawAdapter public weEthWithdrawAdapterImplementation; + PriorityWithdrawalQueue public priorityQueueImplementation; + PriorityWithdrawalQueue public priorityQueueInstance; + EtherFiRewardsRouter public etherFiRewardsRouterInstance = EtherFiRewardsRouter(payable(0x73f7b1184B5cD361cC0f7654998953E2a251dd58)); EtherFiNode public node; @@ -435,39 +439,6 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { weEthWithdrawAdapterInstance = IWeETHWithdrawAdapter(deployed.WEETH_WITHDRAW_ADAPTER()); } - function upgradeEtherFiRedemptionManager() public { - address ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - EtherFiRedemptionManager Implementation = new EtherFiRedemptionManager(address(payable(liquidityPoolInstance)), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance)); - EtherFiRestaker restakerImplementation = new EtherFiRestaker(address(eigenLayerRewardsCoordinator), address(etherFiRedemptionManagerInstance)); - vm.startPrank(owner); - etherFiRestakerInstance.upgradeTo(address(restakerImplementation)); - vm.stopPrank(); - vm.prank(owner); - etherFiRedemptionManagerInstance.upgradeTo(address(Implementation)); - address[] memory _tokens = new address[](2); - _tokens[0] = ETH_ADDRESS; - _tokens[1] = address(etherFiRestakerInstance.lido()); - uint16[] memory _exitFeeSplitToTreasuryInBps = new uint16[](2); - _exitFeeSplitToTreasuryInBps[0] = 10_00; - _exitFeeSplitToTreasuryInBps[1] = 10_00; - uint16[] memory _exitFeeInBps = new uint16[](2); - _exitFeeInBps[0] = 1_00; - _exitFeeInBps[1] = 1_00; - uint16[] memory _lowWatermarkInBpsOfTvl = new uint16[](2); - _lowWatermarkInBpsOfTvl[0] = 1_00; - _lowWatermarkInBpsOfTvl[1] = 50; - uint256[] memory _bucketCapacity = new uint256[](2); - _bucketCapacity[0] = 2000 ether; - _bucketCapacity[1] = 2000 ether; - uint256[] memory _bucketRefillRate = new uint256[](2); - _bucketRefillRate[0] = 0.3 ether; - _bucketRefillRate[1] = 0.3 ether; - vm.startPrank(owner); - roleRegistryInstance.grantRole(keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"), owner); - etherFiRedemptionManagerInstance.initializeTokenParameters(_tokens, _exitFeeSplitToTreasuryInBps, _exitFeeInBps, _lowWatermarkInBpsOfTvl, _bucketCapacity, _bucketRefillRate); - vm.stopPrank(); - } - function updateShouldSetRoleRegistry(bool shouldSetup) public { shouldSetupRoleRegistry = shouldSetup; } @@ -696,7 +667,7 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); - etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance))), ""); + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance), address(priorityQueueInstance))), ""); etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); roleRegistryInstance.grantRole(keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"), owner); // etherFiRedemptionManagerInstance.initializeTokenParameters(_tokens, _exitFeeSplitToTreasuryInBps, _exitFeeInBps, _lowWatermarkInBpsOfTvl, _bucketCapacity, _bucketRefillRate); @@ -913,7 +884,10 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { // upgrade our existing contracts to utilize `roleRegistry` vm.stopPrank(); vm.startPrank(owner); - EtherFiRedemptionManager etherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance)); + PriorityWithdrawalQueue priorityQueueImplementation = new PriorityWithdrawalQueue(address(liquidityPoolInstance), address(eETHInstance), address(roleRegistryInstance), address(treasuryInstance), 1 hours); + UUPSProxy priorityQueueProxy = new UUPSProxy(address(priorityQueueImplementation), ""); + priorityQueueInstance = PriorityWithdrawalQueue(address(priorityQueueProxy)); + EtherFiRedemptionManager etherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance), address(priorityQueueInstance)); etherFiRedemptionManagerProxy = new UUPSProxy(address(etherFiRedemptionManagerImplementation), ""); etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark From 80b99a363f837c405126b368c2798c04c3dc2666 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Thu, 29 Jan 2026 14:54:04 -0500 Subject: [PATCH 16/19] feat: Updated the permit handling logic to revert with a specific error when permit fails due to low allowance. - Added new tests to validate withdrawal requests using permits, including scenarios for invalid permits, expired deadlines, and replay attacks. --- src/PriorityWithdrawalQueue.sol | 4 +- test/PriorityWithdrawalQueue.t.sol | 157 +++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 398528090..87a3488c1 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -210,7 +210,9 @@ contract PriorityWithdrawalQueue is if (amountOfEEth < MIN_AMOUNT) revert InvalidAmount(); (uint256 lpEthBefore, uint256 queueEEthSharesBefore) = _snapshotBalances(); - try eETH.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + try eETH.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch { + revert PermitFailedAndAllowanceTooLow(); + } IERC20(address(eETH)).safeTransferFrom(msg.sender, address(this), amountOfEEth); diff --git a/test/PriorityWithdrawalQueue.t.sol b/test/PriorityWithdrawalQueue.t.sol index f186ce9f6..380eef753 100644 --- a/test/PriorityWithdrawalQueue.t.sol +++ b/test/PriorityWithdrawalQueue.t.sol @@ -173,6 +173,163 @@ contract PriorityWithdrawalQueueTest is TestSetup { assertEq(priorityQueue.totalActiveRequests(), 1, "Should have 1 active request"); } + function test_requestWithdrawWithPermit() public { + uint256 userPrivKey = 999; + address permitUser = vm.addr(userPrivKey); + uint96 withdrawAmount = 1 ether; + + // Whitelist and fund the permit user + vm.prank(alice); + priorityQueue.addToWhitelist(permitUser); + vm.deal(permitUser, 10 ether); + vm.prank(permitUser); + liquidityPoolInstance.deposit{value: 5 ether}(); + + // Record initial state + uint256 initialEethBalance = eETHInstance.balanceOf(permitUser); + uint256 initialQueueEethBalance = eETHInstance.balanceOf(address(priorityQueue)); + uint96 initialNonce = priorityQueue.nonce(); + + // Create valid permit + IPriorityWithdrawalQueue.PermitInput memory permit = _createEEthPermitInput( + userPrivKey, + address(priorityQueue), + withdrawAmount, + eETHInstance.nonces(permitUser), + block.timestamp + 1 hours + ); + + // Request withdrawal with permit + vm.prank(permitUser); + bytes32 requestId = priorityQueue.requestWithdrawWithPermit(withdrawAmount, 0, permit); + + // Verify state changes + assertEq(priorityQueue.nonce(), initialNonce + 1, "Nonce should increment"); + assertApproxEqAbs(eETHInstance.balanceOf(permitUser), initialEethBalance - withdrawAmount, 2, "User eETH balance should decrease"); + assertApproxEqAbs(eETHInstance.balanceOf(address(priorityQueue)), initialQueueEethBalance + withdrawAmount, 2, "Queue eETH balance should increase"); + assertTrue(priorityQueue.requestExists(requestId), "Request should exist"); + } + + function test_requestWithdrawWithPermit_invalidPermit_reverts() public { + uint256 userPrivKey = 999; + address permitUser = vm.addr(userPrivKey); + uint96 withdrawAmount = 1 ether; + + // Whitelist and fund the permit user + vm.prank(alice); + priorityQueue.addToWhitelist(permitUser); + vm.deal(permitUser, 10 ether); + vm.prank(permitUser); + liquidityPoolInstance.deposit{value: 5 ether}(); + + // Create invalid permit (wrong signature) + IPriorityWithdrawalQueue.PermitInput memory invalidPermit = IPriorityWithdrawalQueue.PermitInput({ + value: withdrawAmount, + deadline: block.timestamp + 1 hours, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + // Request should revert with PermitFailedAndAllowanceTooLow + vm.prank(permitUser); + vm.expectRevert(PriorityWithdrawalQueue.PermitFailedAndAllowanceTooLow.selector); + priorityQueue.requestWithdrawWithPermit(withdrawAmount, 0, invalidPermit); + } + + function test_requestWithdrawWithPermit_expiredDeadline_reverts() public { + uint256 userPrivKey = 999; + address permitUser = vm.addr(userPrivKey); + uint96 withdrawAmount = 1 ether; + + // Whitelist and fund the permit user + vm.prank(alice); + priorityQueue.addToWhitelist(permitUser); + vm.deal(permitUser, 10 ether); + vm.prank(permitUser); + liquidityPoolInstance.deposit{value: 5 ether}(); + + // Create permit with expired deadline + IPriorityWithdrawalQueue.PermitInput memory expiredPermit = _createEEthPermitInput( + userPrivKey, + address(priorityQueue), + withdrawAmount, + eETHInstance.nonces(permitUser), + block.timestamp - 1 // expired + ); + + // Request should revert with PermitFailedAndAllowanceTooLow + vm.prank(permitUser); + vm.expectRevert(PriorityWithdrawalQueue.PermitFailedAndAllowanceTooLow.selector); + priorityQueue.requestWithdrawWithPermit(withdrawAmount, 0, expiredPermit); + } + + function test_requestWithdrawWithPermit_replayAttack_reverts() public { + uint256 userPrivKey = 999; + address permitUser = vm.addr(userPrivKey); + uint96 withdrawAmount = 1 ether; + + // Whitelist and fund the permit user + vm.prank(alice); + priorityQueue.addToWhitelist(permitUser); + vm.deal(permitUser, 10 ether); + vm.prank(permitUser); + liquidityPoolInstance.deposit{value: 5 ether}(); + + // Create valid permit + IPriorityWithdrawalQueue.PermitInput memory permit = _createEEthPermitInput( + userPrivKey, + address(priorityQueue), + withdrawAmount, + eETHInstance.nonces(permitUser), + block.timestamp + 1 hours + ); + + // First request should succeed + vm.prank(permitUser); + priorityQueue.requestWithdrawWithPermit(withdrawAmount, 0, permit); + + // Second request with same permit should revert (nonce already used) + vm.prank(permitUser); + vm.expectRevert(PriorityWithdrawalQueue.PermitFailedAndAllowanceTooLow.selector); + priorityQueue.requestWithdrawWithPermit(withdrawAmount, 0, permit); + } + + /// @dev Helper to create eETH permit input + function _createEEthPermitInput( + uint256 privKey, + address spender, + uint256 value, + uint256 nonce, + uint256 deadline + ) internal view returns (IPriorityWithdrawalQueue.PermitInput memory) { + address _owner = vm.addr(privKey); + bytes32 domainSeparator = eETHInstance.DOMAIN_SEPARATOR(); + bytes32 digest = _calculatePermitDigest(_owner, spender, value, nonce, deadline, domainSeparator); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); + return IPriorityWithdrawalQueue.PermitInput({ + value: value, + deadline: deadline, + v: v, + r: r, + s: s + }); + } + + /// @dev Calculate EIP-2612 permit digest + function _calculatePermitDigest( + address _owner, + address spender, + uint256 value, + uint256 nonce, + uint256 deadline, + bytes32 domainSeparator + ) internal pure returns (bytes32) { + bytes32 PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, _owner, spender, value, nonce, deadline)); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + //-------------------------------------------------------------------------------------- //------------------------------ FULFILL TESTS --------------------------------------- From 7385ac9a01798a5f36862ec0bd91f152b61f2e45 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 2 Feb 2026 09:54:50 -0500 Subject: [PATCH 17/19] feat: Add deployment and transaction scripts for PriorityWithdrawalQueue --- .../priority-queue/deployPriorityQueue.s.sol | 193 +++++++++++++++ .../transactionsPriorityQueue.s.sol | 220 ++++++++++++++++++ script/utils/utils.sol | 6 + 3 files changed, 419 insertions(+) create mode 100644 script/upgrades/priority-queue/deployPriorityQueue.s.sol create mode 100644 script/upgrades/priority-queue/transactionsPriorityQueue.s.sol diff --git a/script/upgrades/priority-queue/deployPriorityQueue.s.sol b/script/upgrades/priority-queue/deployPriorityQueue.s.sol new file mode 100644 index 000000000..a5a5d65b6 --- /dev/null +++ b/script/upgrades/priority-queue/deployPriorityQueue.s.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/Script.sol"; +import {LiquidityPool} from "../../../src/LiquidityPool.sol"; +import {PriorityWithdrawalQueue} from "../../../src/PriorityWithdrawalQueue.sol"; +import {UUPSProxy} from "../../../src/UUPSProxy.sol"; +import {Utils, ICreate2Factory} from "../../utils/utils.sol"; + +contract DeployPriorityQueue is Script, Utils { + ICreate2Factory constant factory = ICreate2Factory(0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99); + + address priorityWithdrawalQueueImpl; + address priorityWithdrawalQueueProxy; + address liquidityPoolImpl; + + bytes32 commitHashSalt = hex"45312df178d6eb8143604e47b7aa9e618779c0de"; // TODO: Update with actual commit hash + + // PriorityWithdrawalQueue config + uint32 constant MIN_DELAY = 1 hours; // TODO: Set appropriate min delay (e.g., 1 hours = 3600) + + /// @notice Dry run to show deployment configuration without actually deploying + /// @dev Run with --fork-url to compute Create2 addresses: forge script ... --fork-url + function dryRun() public view { + console2.log("================================================"); + console2.log("============= DRY RUN - CONFIG ============"); + console2.log("================================================"); + console2.log(""); + + console2.log("Constructor Args for PriorityWithdrawalQueue:"); + console2.log(" _liquidityPool:", LIQUIDITY_POOL); + console2.log(" _eETH:", EETH); + console2.log(" _roleRegistry:", ROLE_REGISTRY); + console2.log(" _treasury:", TREASURY); + console2.log(" _minDelay:", MIN_DELAY); + console2.log(""); + + console2.log("Constructor Args for LiquidityPool:"); + console2.log(" _priorityWithdrawalQueue: "); + console2.log(""); + + console2.log("Salt:", vm.toString(commitHashSalt)); + console2.log(""); + + console2.log("To compute exact addresses, run with mainnet fork:"); + console2.log(" forge script script/upgrades/priority-queue/deployPriorityQueue.s.sol:DeployPriorityQueue --sig 'dryRunWithFork()' --fork-url "); + } + + /// @notice Dry run with fork to predict all deployment addresses + function dryRunWithFork() public view { + console2.log("================================================"); + console2.log("============= DRY RUN - PREDICTIONS ============"); + console2.log("================================================"); + console2.log(""); + + // Predict LiquidityPool implementation address + address predictedProxyAddress = _predictPriorityQueueProxyAddress(); + + bytes memory lpConstructorArgs = abi.encode(predictedProxyAddress); + bytes memory lpBytecode = abi.encodePacked( + type(LiquidityPool).creationCode, + lpConstructorArgs + ); + address predictedLpImpl = factory.computeAddress(commitHashSalt, lpBytecode); + + // Predict PriorityWithdrawalQueue implementation address + bytes memory pwqConstructorArgs = abi.encode( + LIQUIDITY_POOL, + EETH, + ROLE_REGISTRY, + TREASURY, + MIN_DELAY + ); + bytes memory pwqBytecode = abi.encodePacked( + type(PriorityWithdrawalQueue).creationCode, + pwqConstructorArgs + ); + address predictedPwqImpl = factory.computeAddress(commitHashSalt, pwqBytecode); + + console2.log("Predicted Addresses:"); + console2.log(" LiquidityPool Implementation:", predictedLpImpl); + console2.log(" PriorityWithdrawalQueue Implementation:", predictedPwqImpl); + console2.log(" PriorityWithdrawalQueue Proxy:", predictedProxyAddress); + console2.log(""); + console2.log("Constructor Args:"); + console2.log(" LiquidityPool._priorityWithdrawalQueue:", predictedProxyAddress); + console2.log(" PriorityWithdrawalQueue._liquidityPool:", LIQUIDITY_POOL); + console2.log(" PriorityWithdrawalQueue._eETH:", EETH); + console2.log(" PriorityWithdrawalQueue._roleRegistry:", ROLE_REGISTRY); + console2.log(" PriorityWithdrawalQueue._treasury:", TREASURY); + console2.log(" PriorityWithdrawalQueue._minDelay:", MIN_DELAY); + console2.log(""); + console2.log("Salt:", vm.toString(commitHashSalt)); + } + + function run() public { + console2.log("================================================"); + console2.log("======== Deploying Priority Queue & LP ========="); + console2.log("================================================"); + console2.log(""); + + // Step 1: Predict PriorityWithdrawalQueue proxy address + address predictedProxyAddress = _predictPriorityQueueProxyAddress(); + console2.log("Predicted PriorityWithdrawalQueue proxy:", predictedProxyAddress); + + vm.startBroadcast(); + + // Step 2: Deploy LiquidityPool implementation with predicted proxy address + { + string memory contractName = "LiquidityPool"; + bytes memory constructorArgs = abi.encode(predictedProxyAddress); + bytes memory bytecode = abi.encodePacked( + type(LiquidityPool).creationCode, + constructorArgs + ); + liquidityPoolImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + // Step 3: Deploy PriorityWithdrawalQueue implementation + { + string memory contractName = "PriorityWithdrawalQueue"; + bytes memory constructorArgs = abi.encode( + LIQUIDITY_POOL, + EETH, + ROLE_REGISTRY, + TREASURY, + MIN_DELAY + ); + bytes memory bytecode = abi.encodePacked( + type(PriorityWithdrawalQueue).creationCode, + constructorArgs + ); + priorityWithdrawalQueueImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + // Step 4: Deploy PriorityWithdrawalQueue proxy with initialization + { + string memory contractName = "UUPSProxy"; // Use actual contract name for artifact lookup + // Encode initialize() call for proxy deployment + bytes memory initData = abi.encodeWithSelector(PriorityWithdrawalQueue.initialize.selector); + bytes memory constructorArgs = abi.encode(priorityWithdrawalQueueImpl, initData); + bytes memory bytecode = abi.encodePacked( + type(UUPSProxy).creationCode, + constructorArgs + ); + priorityWithdrawalQueueProxy = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + + require(priorityWithdrawalQueueProxy == predictedProxyAddress, "Proxy address mismatch!"); + } + + vm.stopBroadcast(); + + // Summary + console2.log(""); + console2.log("================================================"); + console2.log("============== DEPLOYMENT SUMMARY =============="); + console2.log("================================================"); + console2.log("LiquidityPool Implementation:", liquidityPoolImpl); + console2.log("PriorityWithdrawalQueue Implementation:", priorityWithdrawalQueueImpl); + console2.log("PriorityWithdrawalQueue Proxy:", priorityWithdrawalQueueProxy); + console2.log(""); + console2.log("NEXT STEPS:"); + console2.log("1. Initialize PriorityWithdrawalQueue proxy"); + console2.log("2. Upgrade LiquidityPool proxy to new implementation"); + console2.log("3. Grant necessary roles in RoleRegistry"); + } + + /// @notice Predict the PriorityWithdrawalQueue proxy address before deployment + function _predictPriorityQueueProxyAddress() internal view returns (address) { + // First predict implementation address + bytes memory implConstructorArgs = abi.encode( + LIQUIDITY_POOL, + EETH, + ROLE_REGISTRY, + TREASURY, + MIN_DELAY + ); + bytes memory implBytecode = abi.encodePacked( + type(PriorityWithdrawalQueue).creationCode, + implConstructorArgs + ); + address predictedImpl = factory.computeAddress(commitHashSalt, implBytecode); + + // Then predict proxy address (with initialization data) + bytes memory initData = abi.encodeWithSelector(PriorityWithdrawalQueue.initialize.selector); + bytes memory proxyConstructorArgs = abi.encode(predictedImpl, initData); + bytes memory proxyBytecode = abi.encodePacked( + type(UUPSProxy).creationCode, + proxyConstructorArgs + ); + return factory.computeAddress(commitHashSalt, proxyBytecode); + } +} \ No newline at end of file diff --git a/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol b/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol new file mode 100644 index 000000000..68f2fcd32 --- /dev/null +++ b/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "../../utils/utils.sol"; +import "../../../src/EtherFiTimelock.sol"; +import "../../../src/LiquidityPool.sol"; +import "../../../src/PriorityWithdrawalQueue.sol"; +import "../../../src/RoleRegistry.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; + +/// @title PriorityQueueTransactions +/// @notice Generates timelock transactions for upgrading LiquidityPool and granting roles for PriorityWithdrawalQueue +/// @dev Run with: forge script script/upgrades/priority-queue/transactionsPriorityQueue.s.sol --fork-url $MAINNET_RPC_URL +contract PriorityQueueTransactions is Script, Utils { + //-------------------------------------------------------------------------------------- + //------------------------------- EXISTING CONTRACTS ----------------------------------- + //-------------------------------------------------------------------------------------- + EtherFiTimelock etherFiTimelock = EtherFiTimelock(payable(UPGRADE_TIMELOCK)); + RoleRegistry roleRegistryContract = RoleRegistry(ROLE_REGISTRY); + LiquidityPool liquidityPool = LiquidityPool(payable(LIQUIDITY_POOL)); + + //-------------------------------------------------------------------------------------- + //------------------------------- NEW DEPLOYMENTS -------------------------------------- + //-------------------------------------------------------------------------------------- + + // TODO: Update these addresses with actual deployed addresses + address constant liquidityPoolImpl = 0x5598b8c76BA17253459e069041349704c28d33DF; + address constant priorityWithdrawalQueueProxy = 0x79Eb9c078fA5a5Bd1Ee8ba84937acd48AA5F90A8; + address constant priorityWithdrawalQueueImpl = 0xB149ce3957370066D7C03e5CA81A7997Fe00cAF6; + + //-------------------------------------------------------------------------------------- + //------------------------------- ROLES ------------------------------------------------ + //-------------------------------------------------------------------------------------- + + bytes32 public PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE; + bytes32 public PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE; + bytes32 public PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE; + + function run() public { + console2.log("================================================"); + console2.log("Running Priority Queue Transactions"); + console2.log("================================================"); + console2.log(""); + + // string memory forkUrl = vm.envString("MAINNET_RPC_URL"); + // vm.selectFork(vm.createFork(forkUrl)); + + // Get role hashes from the implementation + PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE = PriorityWithdrawalQueue(payable(priorityWithdrawalQueueImpl)).PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE(); + PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE = PriorityWithdrawalQueue(payable(priorityWithdrawalQueueImpl)).PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE(); + PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE = PriorityWithdrawalQueue(payable(priorityWithdrawalQueueImpl)).PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE(); + + executeUpgrade(); + forkTest(); + } + + function executeUpgrade() public { + console2.log("Generating Upgrade Transactions"); + console2.log("================================================"); + + address[] memory targets = new address[](4); + bytes[] memory data = new bytes[](4); + uint256[] memory values = new uint256[](4); // Default to 0 + + //-------------------------------------------------------------------------------------- + //------------------------------- CONTRACT UPGRADES ----------------------------------- + //-------------------------------------------------------------------------------------- + + // Upgrade LiquidityPool to new implementation with priorityWithdrawalQueue support + targets[0] = LIQUIDITY_POOL; + data[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, liquidityPoolImpl); + + //-------------------------------------------------------------------------------------- + //---------------------------------- Grant Roles --------------------------------------- + //-------------------------------------------------------------------------------------- + console2.log("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE:", vm.toString(PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE)); + console2.log("PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE:", vm.toString(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE)); + console2.log("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE:", vm.toString(PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE)); + + // Grant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE to ADMIN_EOA + targets[1] = ROLE_REGISTRY; + data[1] = _encodeRoleGrant( + PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, + ETHERFI_OPERATING_ADMIN + ); + + // Grant PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE to ADMIN_EOA + targets[2] = ROLE_REGISTRY; + data[2] = _encodeRoleGrant( + PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, + ETHERFI_OPERATING_ADMIN + ); + + // Grant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE to ADMIN_EOA + targets[3] = ROLE_REGISTRY; + data[3] = _encodeRoleGrant( + PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE, + ADMIN_EOA + ); + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + + // Generate schedule calldata + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0), // predecessor + timelockSalt, + MIN_DELAY_TIMELOCK // 72 hours + ); + + console2.log("================================================"); + console2.log("Timelock Address:", address(etherFiTimelock)); + console2.log("================================================"); + console2.log(""); + + console2.log("Schedule Tx (call from UPGRADE_ADMIN):"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + // Generate execute calldata + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0), // predecessor + timelockSalt + ); + console2.log("Execute Tx (after 72 hours):"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // Log individual transactions for clarity + console2.log("Transaction Details:"); + console2.log("--------------------"); + console2.log("1. Upgrade LiquidityPool to:", liquidityPoolImpl); + console2.log("2. Grant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE to:", ETHERFI_OPERATING_ADMIN); + console2.log("3. Grant PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE to:", ETHERFI_OPERATING_ADMIN); + console2.log("4. Grant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE to:", ADMIN_EOA); + console2.log("================================================"); + console2.log(""); + + // Execute on fork for testing + console2.log("=== SCHEDULING BATCH ON FORK ==="); + vm.startPrank(ETHERFI_UPGRADE_ADMIN); + etherFiTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_TIMELOCK); + + vm.warp(block.timestamp + MIN_DELAY_TIMELOCK + 1); + etherFiTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + vm.stopPrank(); + + console2.log("Upgrade executed successfully on fork"); + console2.log("================================================"); + } + + function forkTest() public { + console2.log("Running Fork Tests"); + console2.log("================================================"); + + // Verify LiquidityPool upgrade + address impl = liquidityPool.getImplementation(); + require(impl == liquidityPoolImpl, "LiquidityPool implementation mismatch"); + console2.log("LiquidityPool implementation verified:", impl); + + // Verify priorityWithdrawalQueue is set correctly in LiquidityPool + address pwq = liquidityPool.priorityWithdrawalQueue(); + require(pwq == priorityWithdrawalQueueProxy, "PriorityWithdrawalQueue address mismatch"); + console2.log("PriorityWithdrawalQueue in LiquidityPool:", pwq); + + // Verify roles granted + require( + roleRegistryContract.hasRole(PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, ETHERFI_OPERATING_ADMIN), + "ADMIN_EOA does not have PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE" + ); + console2.log("PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE granted to ADMIN_EOA"); + + require( + roleRegistryContract.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, ETHERFI_OPERATING_ADMIN), + "ADMIN_EOA does not have PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE" + ); + console2.log("PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE granted to ADMIN_EOA"); + + require( + roleRegistryContract.hasRole(PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE, ADMIN_EOA), + "ADMIN_EOA does not have PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE" + ); + console2.log("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE granted to ADMIN_EOA"); + + // Test PriorityWithdrawalQueue is accessible via proxy + PriorityWithdrawalQueue pwqContract = PriorityWithdrawalQueue(payable(priorityWithdrawalQueueProxy)); + require(address(pwqContract.liquidityPool()) == LIQUIDITY_POOL, "LiquidityPool reference mismatch in PriorityWithdrawalQueue"); + console2.log("PriorityWithdrawalQueue liquidityPool reference verified"); + + console2.log(""); + console2.log("All fork tests passed!"); + console2.log("================================================"); + } + + //-------------------------------------------------------------------------------------- + //------------------------------- HELPER FUNCTIONS ----------------------------------- + //-------------------------------------------------------------------------------------- + + function _encodeRoleGrant( + bytes32 role, + address account + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector( + RoleRegistry.grantRole.selector, + role, + account + ); + } +} diff --git a/script/utils/utils.sol b/script/utils/utils.sol index 66c4b9daa..4de9bb425 100644 --- a/script/utils/utils.sol +++ b/script/utils/utils.sol @@ -272,6 +272,12 @@ contract Utils is Script, Deployed { return vm.toString(address(uint160(uint256(chunk)))); } else if (compare(t, "uint256")) { return vm.toString(uint256(chunk)); + } else if (compare(t, "uint32")) { + return vm.toString(uint32(uint256(chunk))); + } else if (compare(t, "uint64")) { + return vm.toString(uint64(uint256(chunk))); + } else if (compare(t, "uint128")) { + return vm.toString(uint128(uint256(chunk))); } else if (compare(t, "bool")) { return uint256(chunk) != 0 ? "true" : "false"; } else if (compare(t, "bytes32")) { From ac99058443d8dd25cf482f51fff9ba67798cf8f7 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 2 Feb 2026 13:02:43 -0500 Subject: [PATCH 18/19] refactor: Update test setups and scripts to fix broken tests --- .../upgrades/CrossPodApproval/transactions.s.sol | 4 ++-- .../transactions-reaudit-fixes.s.sol | 2 +- test/RestakingRewardsRouter.t.sol | 2 +- test/TestSetup.sol | 15 ++++++++++++--- .../pectra-fork-tests/EL-withdrawals.t.sol | 4 ++-- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/script/upgrades/CrossPodApproval/transactions.s.sol b/script/upgrades/CrossPodApproval/transactions.s.sol index e38d75420..1402a38f3 100644 --- a/script/upgrades/CrossPodApproval/transactions.s.sol +++ b/script/upgrades/CrossPodApproval/transactions.s.sol @@ -256,13 +256,13 @@ contract CrossPodApprovalScript is Script, Deployed, Utils { } function verifyBytecode() internal { - LiquidityPool newLiquidityPoolImplementation = new LiquidityPool(); + // LiquidityPool newLiquidityPoolImplementation = new LiquidityPool(); EtherFiNodesManager newEtherFiNodesManagerImplementation = new EtherFiNodesManager( address(STAKING_MANAGER), address(ROLE_REGISTRY), address(ETHERFI_RATE_LIMITER) ); - contractCodeChecker.verifyContractByteCodeMatch(liquidityPoolImpl, address(newLiquidityPoolImplementation)); + // contractCodeChecker.verifyContractByteCodeMatch(liquidityPoolImpl, address(newLiquidityPoolImplementation)); contractCodeChecker.verifyContractByteCodeMatch(etherFiNodesManagerImpl, address(newEtherFiNodesManagerImplementation)); console2.log("[OK] Bytecode verified successfully"); diff --git a/script/upgrades/reaudit-fixes/transactions-reaudit-fixes.s.sol b/script/upgrades/reaudit-fixes/transactions-reaudit-fixes.s.sol index a444f92fa..9ebff5992 100644 --- a/script/upgrades/reaudit-fixes/transactions-reaudit-fixes.s.sol +++ b/script/upgrades/reaudit-fixes/transactions-reaudit-fixes.s.sol @@ -82,7 +82,7 @@ contract ReauditFixesTransactions is Utils { console2.log(""); EtherFiNode newEtherFiNodeImplementation = new EtherFiNode(address(LIQUIDITY_POOL), address(ETHERFI_NODES_MANAGER), address(EIGENLAYER_POD_MANAGER), address(EIGENLAYER_DELEGATION_MANAGER), address(ROLE_REGISTRY)); - EtherFiRedemptionManager newEtherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(LIQUIDITY_POOL), address(EETH), address(WEETH), address(TREASURY), address(ROLE_REGISTRY), address(ETHERFI_RESTAKER)); + EtherFiRedemptionManager newEtherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(LIQUIDITY_POOL), address(EETH), address(WEETH), address(TREASURY), address(ROLE_REGISTRY), address(ETHERFI_RESTAKER), address(0x0)); EtherFiRestaker newEtherFiRestakerImplementation = new EtherFiRestaker(address(EIGENLAYER_REWARDS_COORDINATOR), address(ETHERFI_REDEMPTION_MANAGER)); EtherFiRewardsRouter newEtherFiRewardsRouterImplementation = new EtherFiRewardsRouter(address(LIQUIDITY_POOL), address(TREASURY), address(ROLE_REGISTRY)); Liquifier newLiquifierImplementation = new Liquifier(); diff --git a/test/RestakingRewardsRouter.t.sol b/test/RestakingRewardsRouter.t.sol index b8bec9fe4..f6a7e4b48 100644 --- a/test/RestakingRewardsRouter.t.sol +++ b/test/RestakingRewardsRouter.t.sol @@ -56,7 +56,7 @@ contract RestakingRewardsRouterTest is Test { otherToken = new TestERC20("Other Token", "OTH"); // Deploy LiquidityPool - liquidityPoolImpl = new LiquidityPool(); + liquidityPoolImpl = new LiquidityPool(address(0x0)); liquidityPoolProxy = new UUPSProxy(address(liquidityPoolImpl), ""); liquidityPool = LiquidityPool(payable(address(liquidityPoolProxy))); diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 8f4f3439f..85c988922 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -431,12 +431,17 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { etherFiTimelockInstance = EtherFiTimelock(payable(addressProviderInstance.getContractAddress("EtherFiTimelock"))); etherFiAdminInstance = EtherFiAdmin(payable(addressProviderInstance.getContractAddress("EtherFiAdmin"))); etherFiOracleInstance = EtherFiOracle(payable(addressProviderInstance.getContractAddress("EtherFiOracle"))); - etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(address(0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0))); - etherFiRestakerInstance = EtherFiRestaker(payable(address(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf))); roleRegistryInstance = RoleRegistry(addressProviderInstance.getContractAddress("RoleRegistry")); - cumulativeMerkleRewardsDistributorInstance = CumulativeMerkleRewardsDistributor(payable(0x9A8c5046a290664Bf42D065d33512fe403484534)); treasuryInstance = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + etherFiRestakerInstance = EtherFiRestaker(payable(address(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf))); + cumulativeMerkleRewardsDistributorInstance = CumulativeMerkleRewardsDistributor(payable(0x9A8c5046a290664Bf42D065d33512fe403484534)); weEthWithdrawAdapterInstance = IWeETHWithdrawAdapter(deployed.WEETH_WITHDRAW_ADAPTER()); + etherFiRedemptionManagerInstance = liquidityPoolInstance.etherFiRedemptionManager(); + + // Deploy PriorityWithdrawalQueue for fork testing (mainnet LP has immutable address(0) for this) + PriorityWithdrawalQueue priorityQueueImplementation = new PriorityWithdrawalQueue(address(liquidityPoolInstance), address(eETHInstance), address(roleRegistryInstance), address(treasuryInstance), 1 hours); + UUPSProxy priorityQueueProxy = new UUPSProxy(address(priorityQueueImplementation), ""); + priorityQueueInstance = PriorityWithdrawalQueue(address(priorityQueueProxy)); } function updateShouldSetRoleRegistry(bool shouldSetup) public { @@ -667,6 +672,10 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); + priorityQueueImplementation = new PriorityWithdrawalQueue(address(liquidityPoolInstance), address(eETHInstance), address(roleRegistryInstance), address(treasuryInstance), 1 hours); + UUPSProxy priorityQueueProxy = new UUPSProxy(address(priorityQueueImplementation), ""); + priorityQueueInstance = PriorityWithdrawalQueue(address(priorityQueueProxy)); + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance), address(priorityQueueInstance))), ""); etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); roleRegistryInstance.grantRole(keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"), owner); diff --git a/test/behaviour-tests/pectra-fork-tests/EL-withdrawals.t.sol b/test/behaviour-tests/pectra-fork-tests/EL-withdrawals.t.sol index 3d5907bb1..1ab1066de 100644 --- a/test/behaviour-tests/pectra-fork-tests/EL-withdrawals.t.sol +++ b/test/behaviour-tests/pectra-fork-tests/EL-withdrawals.t.sol @@ -67,7 +67,7 @@ contract ELExitsTest is TestSetup { legacyIds[0] = 28689; amounts[0] = 0; - vm.prank(address(etherFiOperatingTimelock)); + vm.prank(address(realElExiter)); etherFiNodesManager.linkLegacyValidatorIds(legacyIds, pubkeys); vm.stopPrank(); @@ -110,7 +110,7 @@ contract ELExitsTest is TestSetup { bytes[] memory linkOnlyOneValidatorPubkeys = new bytes[](1); linkOnlyOneValidatorPubkeys[0] = PK_80194; - vm.prank(address(etherFiOperatingTimelock)); + vm.prank(address(realElExiter)); etherFiNodesManager.linkLegacyValidatorIds(linkOnlyOneValidatorlegacyId, linkOnlyOneValidatorPubkeys); vm.stopPrank(); From 79386cdf51246bab1eccab051dda3cf3db103538 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Mon, 2 Feb 2026 16:40:24 -0500 Subject: [PATCH 19/19] feat: Integrate EtherFiRedemptionManager into deployment and transaction scripts for PriorityQueue --- .../priority-queue/deployPriorityQueue.s.sol | 134 +++++------------- .../transactionsPriorityQueue.s.sol | 26 ++-- src/PriorityWithdrawalQueue.sol | 12 -- 3 files changed, 53 insertions(+), 119 deletions(-) diff --git a/script/upgrades/priority-queue/deployPriorityQueue.s.sol b/script/upgrades/priority-queue/deployPriorityQueue.s.sol index a5a5d65b6..389989f79 100644 --- a/script/upgrades/priority-queue/deployPriorityQueue.s.sol +++ b/script/upgrades/priority-queue/deployPriorityQueue.s.sol @@ -6,6 +6,7 @@ import {LiquidityPool} from "../../../src/LiquidityPool.sol"; import {PriorityWithdrawalQueue} from "../../../src/PriorityWithdrawalQueue.sol"; import {UUPSProxy} from "../../../src/UUPSProxy.sol"; import {Utils, ICreate2Factory} from "../../utils/utils.sol"; +import {EtherFiRedemptionManager} from "../../../src/EtherFiRedemptionManager.sol"; contract DeployPriorityQueue is Script, Utils { ICreate2Factory constant factory = ICreate2Factory(0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99); @@ -13,14 +14,11 @@ contract DeployPriorityQueue is Script, Utils { address priorityWithdrawalQueueImpl; address priorityWithdrawalQueueProxy; address liquidityPoolImpl; - + address etherFiRedemptionManagerImpl; bytes32 commitHashSalt = hex"45312df178d6eb8143604e47b7aa9e618779c0de"; // TODO: Update with actual commit hash - // PriorityWithdrawalQueue config uint32 constant MIN_DELAY = 1 hours; // TODO: Set appropriate min delay (e.g., 1 hours = 3600) - /// @notice Dry run to show deployment configuration without actually deploying - /// @dev Run with --fork-url to compute Create2 addresses: forge script ... --fork-url function dryRun() public view { console2.log("================================================"); console2.log("============= DRY RUN - CONFIG ============"); @@ -46,77 +44,15 @@ contract DeployPriorityQueue is Script, Utils { console2.log(" forge script script/upgrades/priority-queue/deployPriorityQueue.s.sol:DeployPriorityQueue --sig 'dryRunWithFork()' --fork-url "); } - /// @notice Dry run with fork to predict all deployment addresses - function dryRunWithFork() public view { - console2.log("================================================"); - console2.log("============= DRY RUN - PREDICTIONS ============"); - console2.log("================================================"); - console2.log(""); - - // Predict LiquidityPool implementation address - address predictedProxyAddress = _predictPriorityQueueProxyAddress(); - - bytes memory lpConstructorArgs = abi.encode(predictedProxyAddress); - bytes memory lpBytecode = abi.encodePacked( - type(LiquidityPool).creationCode, - lpConstructorArgs - ); - address predictedLpImpl = factory.computeAddress(commitHashSalt, lpBytecode); - - // Predict PriorityWithdrawalQueue implementation address - bytes memory pwqConstructorArgs = abi.encode( - LIQUIDITY_POOL, - EETH, - ROLE_REGISTRY, - TREASURY, - MIN_DELAY - ); - bytes memory pwqBytecode = abi.encodePacked( - type(PriorityWithdrawalQueue).creationCode, - pwqConstructorArgs - ); - address predictedPwqImpl = factory.computeAddress(commitHashSalt, pwqBytecode); - - console2.log("Predicted Addresses:"); - console2.log(" LiquidityPool Implementation:", predictedLpImpl); - console2.log(" PriorityWithdrawalQueue Implementation:", predictedPwqImpl); - console2.log(" PriorityWithdrawalQueue Proxy:", predictedProxyAddress); - console2.log(""); - console2.log("Constructor Args:"); - console2.log(" LiquidityPool._priorityWithdrawalQueue:", predictedProxyAddress); - console2.log(" PriorityWithdrawalQueue._liquidityPool:", LIQUIDITY_POOL); - console2.log(" PriorityWithdrawalQueue._eETH:", EETH); - console2.log(" PriorityWithdrawalQueue._roleRegistry:", ROLE_REGISTRY); - console2.log(" PriorityWithdrawalQueue._treasury:", TREASURY); - console2.log(" PriorityWithdrawalQueue._minDelay:", MIN_DELAY); - console2.log(""); - console2.log("Salt:", vm.toString(commitHashSalt)); - } - function run() public { console2.log("================================================"); console2.log("======== Deploying Priority Queue & LP ========="); console2.log("================================================"); console2.log(""); - // Step 1: Predict PriorityWithdrawalQueue proxy address - address predictedProxyAddress = _predictPriorityQueueProxyAddress(); - console2.log("Predicted PriorityWithdrawalQueue proxy:", predictedProxyAddress); - vm.startBroadcast(); - // Step 2: Deploy LiquidityPool implementation with predicted proxy address - { - string memory contractName = "LiquidityPool"; - bytes memory constructorArgs = abi.encode(predictedProxyAddress); - bytes memory bytecode = abi.encodePacked( - type(LiquidityPool).creationCode, - constructorArgs - ); - liquidityPoolImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); - } - - // Step 3: Deploy PriorityWithdrawalQueue implementation + // Step 1: Deploy PriorityWithdrawalQueue implementation { string memory contractName = "PriorityWithdrawalQueue"; bytes memory constructorArgs = abi.encode( @@ -133,7 +69,7 @@ contract DeployPriorityQueue is Script, Utils { priorityWithdrawalQueueImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); } - // Step 4: Deploy PriorityWithdrawalQueue proxy with initialization + // Step 2: Deploy PriorityWithdrawalQueue proxy with initialization { string memory contractName = "UUPSProxy"; // Use actual contract name for artifact lookup // Encode initialize() call for proxy deployment @@ -144,8 +80,36 @@ contract DeployPriorityQueue is Script, Utils { constructorArgs ); priorityWithdrawalQueueProxy = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); - - require(priorityWithdrawalQueueProxy == predictedProxyAddress, "Proxy address mismatch!"); + } + + // Step 3: Deploy EtherFiRedemptionManager implementation + { + string memory contractName = "EtherFiRedemptionManager"; + bytes memory constructorArgs = abi.encode( + LIQUIDITY_POOL, + EETH, + WEETH, + TREASURY, + ROLE_REGISTRY, + ETHERFI_RESTAKER, + priorityWithdrawalQueueProxy + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManager).creationCode, + constructorArgs + ); + etherFiRedemptionManagerImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + // Step 4: Deploy LiquidityPool implementation with predicted proxy address + { + string memory contractName = "LiquidityPool"; + bytes memory constructorArgs = abi.encode(priorityWithdrawalQueueProxy); + bytes memory bytecode = abi.encodePacked( + type(LiquidityPool).creationCode, + constructorArgs + ); + liquidityPoolImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); } vm.stopBroadcast(); @@ -158,36 +122,12 @@ contract DeployPriorityQueue is Script, Utils { console2.log("LiquidityPool Implementation:", liquidityPoolImpl); console2.log("PriorityWithdrawalQueue Implementation:", priorityWithdrawalQueueImpl); console2.log("PriorityWithdrawalQueue Proxy:", priorityWithdrawalQueueProxy); + console2.log("EtherFiRedemptionManager Implementation:", etherFiRedemptionManagerImpl); console2.log(""); console2.log("NEXT STEPS:"); console2.log("1. Initialize PriorityWithdrawalQueue proxy"); console2.log("2. Upgrade LiquidityPool proxy to new implementation"); - console2.log("3. Grant necessary roles in RoleRegistry"); - } - - /// @notice Predict the PriorityWithdrawalQueue proxy address before deployment - function _predictPriorityQueueProxyAddress() internal view returns (address) { - // First predict implementation address - bytes memory implConstructorArgs = abi.encode( - LIQUIDITY_POOL, - EETH, - ROLE_REGISTRY, - TREASURY, - MIN_DELAY - ); - bytes memory implBytecode = abi.encodePacked( - type(PriorityWithdrawalQueue).creationCode, - implConstructorArgs - ); - address predictedImpl = factory.computeAddress(commitHashSalt, implBytecode); - - // Then predict proxy address (with initialization data) - bytes memory initData = abi.encodeWithSelector(PriorityWithdrawalQueue.initialize.selector); - bytes memory proxyConstructorArgs = abi.encode(predictedImpl, initData); - bytes memory proxyBytecode = abi.encodePacked( - type(UUPSProxy).creationCode, - proxyConstructorArgs - ); - return factory.computeAddress(commitHashSalt, proxyBytecode); + console2.log("3. Upgrade EtherFiRedemptionManager proxy to new implementation"); + console2.log("4. Grant necessary roles in RoleRegistry"); } } \ No newline at end of file diff --git a/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol b/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol index 68f2fcd32..83c603e67 100644 --- a/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol +++ b/script/upgrades/priority-queue/transactionsPriorityQueue.s.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.27; import "../../utils/utils.sol"; import "../../../src/EtherFiTimelock.sol"; +import "../../../src/EtherFiRedemptionManager.sol"; import "../../../src/LiquidityPool.sol"; import "../../../src/PriorityWithdrawalQueue.sol"; import "../../../src/RoleRegistry.sol"; @@ -20,6 +21,7 @@ contract PriorityQueueTransactions is Script, Utils { EtherFiTimelock etherFiTimelock = EtherFiTimelock(payable(UPGRADE_TIMELOCK)); RoleRegistry roleRegistryContract = RoleRegistry(ROLE_REGISTRY); LiquidityPool liquidityPool = LiquidityPool(payable(LIQUIDITY_POOL)); + EtherFiRedemptionManager etherFiRedemptionManager = EtherFiRedemptionManager(payable(ETHERFI_REDEMPTION_MANAGER)); //-------------------------------------------------------------------------------------- //------------------------------- NEW DEPLOYMENTS -------------------------------------- @@ -29,7 +31,7 @@ contract PriorityQueueTransactions is Script, Utils { address constant liquidityPoolImpl = 0x5598b8c76BA17253459e069041349704c28d33DF; address constant priorityWithdrawalQueueProxy = 0x79Eb9c078fA5a5Bd1Ee8ba84937acd48AA5F90A8; address constant priorityWithdrawalQueueImpl = 0xB149ce3957370066D7C03e5CA81A7997Fe00cAF6; - + address constant etherFiRedemptionManagerImpl = 0x335E9Cf5A2b13621b66D01F8b889174AD75DE045; //-------------------------------------------------------------------------------------- //------------------------------- ROLES ------------------------------------------------ //-------------------------------------------------------------------------------------- @@ -60,9 +62,9 @@ contract PriorityQueueTransactions is Script, Utils { console2.log("Generating Upgrade Transactions"); console2.log("================================================"); - address[] memory targets = new address[](4); - bytes[] memory data = new bytes[](4); - uint256[] memory values = new uint256[](4); // Default to 0 + address[] memory targets = new address[](5); + bytes[] memory data = new bytes[](targets.length); + uint256[] memory values = new uint256[](targets.length); // Default to 0 //-------------------------------------------------------------------------------------- //------------------------------- CONTRACT UPGRADES ----------------------------------- @@ -72,6 +74,10 @@ contract PriorityQueueTransactions is Script, Utils { targets[0] = LIQUIDITY_POOL; data[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, liquidityPoolImpl); + // Upgrade EtherFiRedemptionManager to new implementation + targets[1] = ETHERFI_REDEMPTION_MANAGER; + data[1] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, etherFiRedemptionManagerImpl); + //-------------------------------------------------------------------------------------- //---------------------------------- Grant Roles --------------------------------------- //-------------------------------------------------------------------------------------- @@ -80,22 +86,22 @@ contract PriorityQueueTransactions is Script, Utils { console2.log("PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE:", vm.toString(PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE)); // Grant PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE to ADMIN_EOA - targets[1] = ROLE_REGISTRY; - data[1] = _encodeRoleGrant( + targets[2] = ROLE_REGISTRY; + data[2] = _encodeRoleGrant( PRIORITY_WITHDRAWAL_QUEUE_ADMIN_ROLE, ETHERFI_OPERATING_ADMIN ); // Grant PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE to ADMIN_EOA - targets[2] = ROLE_REGISTRY; - data[2] = _encodeRoleGrant( + targets[3] = ROLE_REGISTRY; + data[3] = _encodeRoleGrant( PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, ETHERFI_OPERATING_ADMIN ); // Grant PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE to ADMIN_EOA - targets[3] = ROLE_REGISTRY; - data[3] = _encodeRoleGrant( + targets[4] = ROLE_REGISTRY; + data[4] = _encodeRoleGrant( PRIORITY_WITHDRAWAL_QUEUE_REQUEST_MANAGER_ROLE, ADMIN_EOA ); diff --git a/src/PriorityWithdrawalQueue.sol b/src/PriorityWithdrawalQueue.sol index 87a3488c1..c96c814e8 100644 --- a/src/PriorityWithdrawalQueue.sol +++ b/src/PriorityWithdrawalQueue.sol @@ -197,11 +197,6 @@ contract PriorityWithdrawalQueue is _verifyRequestPostConditions(lpEthBefore, queueEEthSharesBefore, amountOfEEth); } - /// @notice Request a withdrawal with permit for gasless approval - /// @param amountOfEEth Amount of eETH to withdraw - /// @param minAmountOut Minimum ETH output amount (slippage protection for dynamic fees) - /// @param permit Permit signature data for eETH approval - /// @return requestId The hash-based ID of the created withdrawal request function requestWithdrawWithPermit( uint96 amountOfEEth, uint96 minAmountOut, @@ -302,8 +297,6 @@ contract PriorityWithdrawalQueue is //----------------------------------- ADMIN FUNCTIONS -------------------------------- //-------------------------------------------------------------------------------------- - /// @notice Add an address to the whitelist - /// @param user Address to whitelist function addToWhitelist(address user) external { if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); if (user == address(0)) revert AddressZero(); @@ -311,17 +304,12 @@ contract PriorityWithdrawalQueue is emit WhitelistUpdated(user, true); } - /// @notice Remove an address from the whitelist - /// @param user Address to remove from whitelist function removeFromWhitelist(address user) external { if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); isWhitelisted[user] = false; emit WhitelistUpdated(user, false); } - /// @notice Batch update whitelist status - /// @param users Array of user addresses - /// @param statuses Array of whitelist statuses function batchUpdateWhitelist(address[] calldata users, bool[] calldata statuses) external { if (!roleRegistry.hasRole(PRIORITY_WITHDRAWAL_QUEUE_WHITELIST_MANAGER_ROLE, msg.sender)) revert IncorrectRole(); if (users.length != statuses.length) revert ArrayLengthMismatch();