From 91569c023c29e0ab3bef82d108f72ee643345dd7 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Wed, 13 Nov 2024 13:53:45 -0600 Subject: [PATCH 1/6] staking manager --- src/EtherFiRestakeManager.sol | 215 +++++++++++++++++++++ src/EtherFiRestaker.sol | 108 ++++------- test/BucketRaterLimiter.t.sol | 2 +- test/EtherFiRestaker.t.sol | 342 +++++++++++++++++----------------- test/TestSetup.sol | 8 +- 5 files changed, 427 insertions(+), 248 deletions(-) create mode 100644 src/EtherFiRestakeManager.sol diff --git a/src/EtherFiRestakeManager.sol b/src/EtherFiRestakeManager.sol new file mode 100644 index 000000000..b58347b46 --- /dev/null +++ b/src/EtherFiRestakeManager.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import "./Liquifier.sol"; +import "./EtherFiRestaker.sol"; + +contract EtherFiRestakeManager is + Initializable, + OwnableUpgradeable, + PausableUpgradeable, + UUPSUpgradeable +{ + + LiquidityPool public liquidityPool; + Liquifier public liquifier; + ILidoWithdrawalQueue public lidoWithdrawalQueue; + ILido public lido; + + mapping(address => bool) public pausers; + mapping(address => bool) public admins; + + UpgradeableBeacon public upgradableBeacon; + uint256 public nextAvsOperatorId; + mapping(uint256 => EtherFiRestaker) public etherFiRestaker; + + event QueuedStEthWithdrawals(uint256[] _reqIds); + event CompletedStEthQueuedWithdrawals(uint256[] _reqIds); + event CreatedEtherFiRestaker(uint256 indexed id, address etherFiRestaker); + + error IncorrectCaller(); + error IncorrectAmount(); + error NotEnoughBalance(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _liquidityPool, address _liquifier) initializer external { + __Ownable_init(); + __Pausable_init(); + __UUPSUpgradeable_init(); + + liquidityPool = LiquidityPool(payable(_liquidityPool)); + liquifier = Liquifier(payable(_liquifier)); + + lido = liquifier.lido(); + lidoWithdrawalQueue = liquifier.lidoWithdrawalQueue(); + } + + function instantiateEtherFiRestaker(uint256 _nums) external onlyOwner returns (uint256[] memory _ids) { + _ids = new uint256[](_nums); + for (uint256 i = 0; i < _nums; i++) { + _ids[i] = _instantiateEtherFiRestaker(); + } + } + + function _instantiateEtherFiRestaker() internal returns (uint256 _id) { + _id = nextAvsOperatorId++; + require(address(etherFiRestaker[_id]) == address(0), "INVALID_ID"); + + BeaconProxy proxy = new BeaconProxy(address(upgradableBeacon), ""); + etherFiRestaker[_id] = EtherFiRestaker(payable(address(proxy))); + etherFiRestaker[_id].initialize(address(liquidityPool), address(liquifier), address(this)); + + emit CreatedEtherFiRestaker(_id, address(etherFiRestaker[_id])); + + return _id; + } + + // |--------------------------------------------------------------------------------------------| + // | EigenLayer Restaking | + // |--------------------------------------------------------------------------------------------| + + function delegateTo(uint256 index, address operator, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) external { + return etherFiRestaker[index].delegateTo(operator, approverSignatureAndExpiry, approverSalt); + } + + function undelegate(uint256 index) external returns (bytes32[] memory) { + return etherFiRestaker[index].undelegate(); + } + + function depositIntoStrategy( + uint256 index, + address token, + uint256 amount + ) external returns (uint256) { + IERC20(token).transfer(address(etherFiRestaker[index]), amount); + return etherFiRestaker[index].depositIntoStrategy(token); + } + + function queueWithdrawals( + uint256 index, + address token, + uint256 amount + ) external returns (bytes32[] memory) { + return etherFiRestaker[index].queueWithdrawals(token, amount); + } + + function queueWithdrawalsAdvanced( + uint256 index, + IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams + ) external returns (bytes32[] memory) { + return etherFiRestaker[index].queueWithdrawals(queuedWithdrawalParams); + } + + function completeQueuedWithdrawals( + uint256 index, + uint256 max_cnt + ) external { + etherFiRestaker[index].completeQueuedWithdrawals(max_cnt); + } + + function completeQueuedWithdrawalsAdvanced( + uint256 index, + IDelegationManager.Withdrawal[] memory _queuedWithdrawals, + IERC20[][] memory _tokens, + uint256[] memory _middlewareTimesIndexes + ) external { + etherFiRestaker[index].completeQueuedWithdrawals( + _queuedWithdrawals, + _tokens, + _middlewareTimesIndexes + ); + } + + // |--------------------------------------------------------------------------------------------| + // | Handling Lido's stETH | + // |--------------------------------------------------------------------------------------------| + + /// Initiate the redemption of stETH for ETH + /// @notice Request for all stETH holdings + function stEthRequestWithdrawal() external onlyAdmin returns (uint256[] memory) { + uint256 amount = lido.balanceOf(address(this)); + return stEthRequestWithdrawal(amount); + } + + /// @notice Request for a specific amount of stETH holdings + /// @param _amount the amount of stETH to request + function stEthRequestWithdrawal(uint256 _amount) public onlyAdmin returns (uint256[] memory) { + if (_amount < lidoWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()) revert IncorrectAmount(); + if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance(); + + uint256 maxAmount = lidoWithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT(); + uint256 numReqs = (_amount + maxAmount - 1) / maxAmount; + uint256[] memory reqAmounts = new uint256[](numReqs); + for (uint256 i = 0; i < numReqs; i++) { + reqAmounts[i] = (i == numReqs - 1) ? _amount - i * maxAmount : maxAmount; + } + lido.approve(address(lidoWithdrawalQueue), _amount); + uint256[] memory reqIds = lidoWithdrawalQueue.requestWithdrawals(reqAmounts, address(this)); + + emit QueuedStEthWithdrawals(reqIds); + + return reqIds; + } + + /// @notice Claim a batch of withdrawal requests if they are finalized sending the ETH to the this contract back + /// @param _requestIds array of request ids to claim + /// @param _hints checkpoint hint for each id. Can be obtained with `findCheckpointHints()` + function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external onlyAdmin { + uint256 balance = address(this).balance; + lidoWithdrawalQueue.claimWithdrawals(_requestIds, _hints); + + withdrawEther(); + + emit CompletedStEthQueuedWithdrawals(_requestIds); + } + + // Send the ETH back to the liquidity pool + function withdrawEther() public onlyAdmin { + uint256 amountToLiquidityPool = address(this).balance; + (bool sent, ) = payable(address(liquidityPool)).call{value: amountToLiquidityPool, gas: 20000}(""); + require(sent, "ETH_SEND_TO_LIQUIDITY_POOL_FAILED"); + } + + // |--------------------------------------------------------------------------------------------| + // | VIEW functions | + // |--------------------------------------------------------------------------------------------| + + /// @notice Returns the total stETH {staked, unstaked} + function getTotalPooledStETH() external view returns (uint256 amount){ + uint256 amount = lido.balanceOf(address(this)); + for (uint256 i = 0; i < nextAvsOperatorId; i++) { + amount += etherFiRestaker[i].getRestakedAmount(); + } + return amount; + } + + receive() external payable {} + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + function _requireAdmin() internal view virtual { + if (!(admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); + } + + function _requirePauser() internal view virtual { + if (!(pausers[msg.sender] || admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); + } + + /* MODIFIER */ + modifier onlyAdmin() { + _requireAdmin(); + _; + } + + modifier onlyPauser() { + _requirePauser(); + _; + } +} diff --git a/src/EtherFiRestaker.sol b/src/EtherFiRestaker.sol index d4d9c5dc0..91aa8be93 100644 --- a/src/EtherFiRestaker.sol +++ b/src/EtherFiRestaker.sol @@ -28,6 +28,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, LiquidityPool public liquidityPool; Liquifier public liquifier; + address public etherFiRestakeManager; ILidoWithdrawalQueue public lidoWithdrawalQueue; ILido public lido; IDelegationManager public eigenLayerDelegationManager; @@ -61,13 +62,14 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, } /// @notice initialize to set variables on deployment - function initialize(address _liquidityPool, address _liquifier) initializer external { + function initialize(address _liquidityPool, address _liquifier, address _manager) initializer external { __Ownable_init(); __Pausable_init(); __UUPSUpgradeable_init(); liquidityPool = LiquidityPool(payable(_liquidityPool)); liquifier = Liquifier(payable(_liquifier)); + etherFiRestakeManager = _manager; lido = liquifier.lido(); lidoWithdrawalQueue = liquifier.lidoWithdrawalQueue(); @@ -84,67 +86,17 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, receive() external payable {} - // |--------------------------------------------------------------------------------------------| - // | Handling Lido's stETH | - // |--------------------------------------------------------------------------------------------| - - /// Initiate the redemption of stETH for ETH - /// @notice Request for all stETH holdings - function stEthRequestWithdrawal() external onlyAdmin returns (uint256[] memory) { - uint256 amount = lido.balanceOf(address(this)); - return stEthRequestWithdrawal(amount); - } - - /// @notice Request for a specific amount of stETH holdings - /// @param _amount the amount of stETH to request - function stEthRequestWithdrawal(uint256 _amount) public onlyAdmin returns (uint256[] memory) { - if (_amount < lidoWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()) revert IncorrectAmount(); - if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance(); - - uint256 maxAmount = lidoWithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT(); - uint256 numReqs = (_amount + maxAmount - 1) / maxAmount; - uint256[] memory reqAmounts = new uint256[](numReqs); - for (uint256 i = 0; i < numReqs; i++) { - reqAmounts[i] = (i == numReqs - 1) ? _amount - i * maxAmount : maxAmount; - } - lido.approve(address(lidoWithdrawalQueue), _amount); - uint256[] memory reqIds = lidoWithdrawalQueue.requestWithdrawals(reqAmounts, address(this)); - - emit QueuedStEthWithdrawals(reqIds); - - return reqIds; - } - - /// @notice Claim a batch of withdrawal requests if they are finalized sending the ETH to the this contract back - /// @param _requestIds array of request ids to claim - /// @param _hints checkpoint hint for each id. Can be obtained with `findCheckpointHints()` - function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external onlyAdmin { - uint256 balance = address(this).balance; - lidoWithdrawalQueue.claimWithdrawals(_requestIds, _hints); - - withdrawEther(); - - emit CompletedStEthQueuedWithdrawals(_requestIds); - } - - // Send the ETH back to the liquidity pool - function withdrawEther() public onlyAdmin { - uint256 amountToLiquidityPool = address(this).balance; - (bool sent, ) = payable(address(liquidityPool)).call{value: amountToLiquidityPool, gas: 20000}(""); - require(sent, "ETH_SEND_TO_LIQUIDITY_POOL_FAILED"); - } - // |--------------------------------------------------------------------------------------------| // | EigenLayer Restaking | // |--------------------------------------------------------------------------------------------| // delegate to an AVS operator - function delegateTo(address operator, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) external onlyAdmin { + function delegateTo(address operator, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) external managerOnly { eigenLayerDelegationManager.delegateTo(operator, approverSignatureAndExpiry, approverSalt); } // undelegate from the current AVS operator & un-restake all - function undelegate() external onlyAdmin returns (bytes32[] memory) { + function undelegate() external managerOnly returns (bytes32[] memory) { // Un-restake all assets // Currently, only stETH is supported TokenInfo memory info = tokenInfos[address(lido)]; @@ -159,7 +111,9 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, } // deposit the token in holding into the restaking strategy - function depositIntoStrategy(address token, uint256 amount) external onlyAdmin returns (uint256) { + function depositIntoStrategy(address token) external managerOnly returns (uint256) { + // using `balanceOf` instead of passing the amount param from `EtherFiRestakeManager.depositIntoStrategy` to avoid 1-2 wei corner case on stETH transfers + uint256 amount = IERC20(token).balanceOf(address(this)); IERC20(token).safeApprove(address(eigenLayerStrategyManager), amount); IStrategy strategy = tokenInfos[token].elStrategy; @@ -172,13 +126,13 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// Made easy for operators /// @param token the token to withdraw /// @param amount the amount of token to withdraw - function queueWithdrawals(address token, uint256 amount) public onlyAdmin returns (bytes32[] memory) { + function queueWithdrawals(address token, uint256 amount) public managerOnly returns (bytes32[] memory) { uint256 shares = getEigenLayerRestakingStrategy(token).underlyingToSharesView(amount); return _queueWithdrawlsByShares(token, shares); } /// Advanced version - function queueWithdrawals(IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams) public onlyAdmin returns (bytes32[] memory) { + function queueWithdrawals(IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams) public managerOnly returns (bytes32[] memory) { uint256 currentNonce = eigenLayerDelegationManager.cumulativeWithdrawalsQueued(address(this)); bytes32[] memory withdrawalRoots = eigenLayerDelegationManager.queueWithdrawals(queuedWithdrawalParams); @@ -212,7 +166,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// @notice Complete the queued withdrawals that are ready to be withdrawn /// @param max_cnt the maximum number of withdrawals to complete - function completeQueuedWithdrawals(uint256 max_cnt) external onlyAdmin { + function completeQueuedWithdrawals(uint256 max_cnt) external managerOnly { bytes32[] memory withdrawalRoots = pendingWithdrawalRoots(); // process the first `max_cnt` withdrawals @@ -262,7 +216,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// @param _tokens Array of tokens for each QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single array. /// @param _middlewareTimesIndexes One index to reference per QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single index. /// @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw` - function completeQueuedWithdrawals(IDelegationManager.Withdrawal[] memory _queuedWithdrawals, IERC20[][] memory _tokens, uint256[] memory _middlewareTimesIndexes) public onlyAdmin { + function completeQueuedWithdrawals(IDelegationManager.Withdrawal[] memory _queuedWithdrawals, IERC20[][] memory _tokens, uint256[] memory _middlewareTimesIndexes) public managerOnly { uint256 num = _queuedWithdrawals.length; bool[] memory receiveAsTokens = new bool[](num); for (uint256 i = 0; i < num; i++) { @@ -276,24 +230,34 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// it will update the erc20 balances of this contract eigenLayerDelegationManager.completeQueuedWithdrawals(_queuedWithdrawals, _tokens, _middlewareTimesIndexes, receiveAsTokens); + + /// transfer tokens back to manager + for (uint256 i = 0; i < _queuedWithdrawals.length; ++i) { + for (uint256 j = 0; j < _tokens[i].length; ++i) { + _tokens[i][j].transfer(etherFiRestakeManager, _tokens[i][j].balanceOf(address(this))); + } + } } + // |--------------------------------------------------------------------------------------------| + // | VIEW functions | + // |--------------------------------------------------------------------------------------------| + /// Enumerate the pending withdrawal roots + // used in complete queued withdrawal function function pendingWithdrawalRoots() public view returns (bytes32[] memory) { return withdrawalRootsSet.values(); } /// Check if a withdrawal is pending for a given withdrawal root + // not directly used in withdrawal logic, seems like it could be useful function isPendingWithdrawal(bytes32 _withdrawalRoot) external view returns (bool) { return withdrawalRootsSet.contains(_withdrawalRoot); } - - // |--------------------------------------------------------------------------------------------| - // | VIEW functions | - // |--------------------------------------------------------------------------------------------| - function getTotalPooledEther() public view returns (uint256 total) { - total = address(this).balance + getTotalPooledEther(address(lido)); + // Returns the amount that this contract has restaked denominated in Ether + function getRestakedAmount() external view returns (uint256 amount) { + return getTotalPooledEther(address(lido)); } function getTotalPooledEther(address _token) public view returns (uint256) { @@ -301,6 +265,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, return restaked + unrestaking + holding + pendingForWithdrawals; } + // used in the getRestakedAmount function function getRestakedAmount(address _token) public view returns (uint256) { TokenInfo memory info = tokenInfos[_token]; uint256 shares = eigenLayerStrategyManager.stakerStrategyShares(address(this), info.elStrategy); @@ -352,7 +317,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, admins[_address] = _isAdmin; } - function updatePauser(address _address, bool _isPauser) external onlyAdmin { + function updatePauser(address _address, bool _isPauser) external managerOnly { pausers[_address] = _isPauser; } @@ -362,7 +327,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, } // Unpauses the contract - function unPauseContract() external onlyAdmin { + function unPauseContract() external managerOnly { _unpause(); } @@ -398,14 +363,13 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, if (!(pausers[msg.sender] || admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); } - /* MODIFIER */ - modifier onlyAdmin() { - _requireAdmin(); + modifier onlyPauser() { + _requirePauser(); _; } - modifier onlyPauser() { - _requirePauser(); + modifier managerOnly() { + require(msg.sender == etherFiRestakeManager, "NOT_MANAGER"); _; } -} \ No newline at end of file +} diff --git a/test/BucketRaterLimiter.t.sol b/test/BucketRaterLimiter.t.sol index 3876abf95..2f944c8a3 100644 --- a/test/BucketRaterLimiter.t.sol +++ b/test/BucketRaterLimiter.t.sol @@ -146,4 +146,4 @@ contract BucketRateLimiterTest is Test { limiter.unPauseContract(); } -} \ No newline at end of file +} diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol index 1af44d8a9..5f3a7e2ce 100644 --- a/test/EtherFiRestaker.t.sol +++ b/test/EtherFiRestaker.t.sol @@ -31,198 +31,198 @@ contract EtherFiRestakerTest is TestSetup { liquifierInstance.updateQuoteStEthWithCurve(false); } - function _deposit_stEth(uint256 _amount) internal { - uint256 restakerTvl = etherFiRestakerInstance.getTotalPooledEther(); - uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - uint256 lpBalance = address(liquidityPoolInstance).balance; - uint256 aliceStEthBalance = stEth.balanceOf(alice); - uint256 aliceEEthBalance = eETHInstance.balanceOf(alice); - - vm.deal(alice, _amount); - vm.startPrank(alice); - stEth.submit{value: _amount}(address(0)); + // function _deposit_stEth(uint256 _amount) internal { + // uint256 restakerTvl = etherFiRestakerInstance.getTotalPooledEther(); + // uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); + // uint256 lpBalance = address(liquidityPoolInstance).balance; + // uint256 aliceStEthBalance = stEth.balanceOf(alice); + // uint256 aliceEEthBalance = eETHInstance.balanceOf(alice); + + // vm.deal(alice, _amount); + // vm.startPrank(alice); + // stEth.submit{value: _amount}(address(0)); - ILiquidityPool.PermitInput memory permitInput = createPermitInput(2, address(liquifierInstance), _amount, stEth.nonces(alice), 2**256 - 1, stEth.DOMAIN_SEPARATOR()); - ILiquifier.PermitInput memory permitInput2 = ILiquifier.PermitInput({value: permitInput.value, deadline: permitInput.deadline, v: permitInput.v, r: permitInput.r, s: permitInput.s}); - liquifierInstance.depositWithERC20WithPermit(address(stEth), _amount, address(0), permitInput2); - - - // Aliice has 10 ether eETH - // Total eETH TVL is 10 ether - assertApproxEqAbs(stEth.balanceOf(alice), aliceStEthBalance, 1 wei); - assertApproxEqAbs(eETHInstance.balanceOf(alice), aliceEEthBalance + _amount, 1 wei); - assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), restakerTvl + _amount, 1 wei); - assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + _amount, 1 wei); - vm.stopPrank(); - } - - function test_withdrawal_of_non_restaked_stEth() public { - uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - uint256 lpBalance = address(liquidityPoolInstance).balance; + // ILiquidityPool.PermitInput memory permitInput = createPermitInput(2, address(liquifierInstance), _amount, stEth.nonces(alice), 2**256 - 1, stEth.DOMAIN_SEPARATOR()); + // ILiquifier.PermitInput memory permitInput2 = ILiquifier.PermitInput({value: permitInput.value, deadline: permitInput.deadline, v: permitInput.v, r: permitInput.r, s: permitInput.s}); + // liquifierInstance.depositWithERC20WithPermit(address(stEth), _amount, address(0), permitInput2); - uint256 amount = 10 ether; - _deposit_stEth(amount); + // // Aliice has 10 ether eETH + // // Total eETH TVL is 10 ether + // assertApproxEqAbs(stEth.balanceOf(alice), aliceStEthBalance, 1 wei); + // assertApproxEqAbs(eETHInstance.balanceOf(alice), aliceEEthBalance + _amount, 1 wei); + // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), restakerTvl + _amount, 1 wei); + // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + _amount, 1 wei); + // vm.stopPrank(); + // } - assertEq(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0); + // function test_withdrawal_of_non_restaked_stEth() public { + // uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); + // uint256 lpBalance = address(liquidityPoolInstance).balance; - vm.startPrank(alice); - uint256 stEthBalance = stEth.balanceOf(address(etherFiRestakerInstance)); - uint256[] memory reqIds = etherFiRestakerInstance.stEthRequestWithdrawal(stEthBalance); - vm.stopPrank(); - - assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 2 wei); - assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), amount, 2 wei); - assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); - - bytes32 FINALIZE_ROLE = etherFiRestakerInstance.lidoWithdrawalQueue().FINALIZE_ROLE(); - address finalize_role = etherFiRestakerInstance.lidoWithdrawalQueue().getRoleMember(FINALIZE_ROLE, 0); - - // The redemption is approved by the Lido - vm.startPrank(finalize_role); - uint256 currentRate = stEth.getTotalPooledEther() * 1e27 / stEth.getTotalShares(); - (uint256 ethToLock, uint256 sharesToBurn) = etherFiRestakerInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate); - etherFiRestakerInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate); - vm.stopPrank(); - - assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 2 wei); - assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), amount, 2 wei); - assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); - - // The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract - vm.startPrank(alice); - uint256 lastCheckPointIndex = etherFiRestakerInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); - uint256[] memory hints = etherFiRestakerInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); - etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); - - // the cycle completes - assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0, 2 wei); - assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), 0, 2 wei); - assertApproxEqAbs(address(etherFiRestakerInstance).balance, 0, 2); - - assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); - assertApproxEqAbs(address(liquidityPoolInstance).balance, lpBalance + amount, 2 wei); - } + // uint256 amount = 10 ether; - function test_restake_stEth() public { - uint256 currentStEthRestakedAmount = etherFiRestakerInstance.getRestakedAmount(address(stEth)); + // _deposit_stEth(amount); - _deposit_stEth(10 ether); + // assertEq(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0); - vm.startPrank(alice); - etherFiRestakerInstance.depositIntoStrategy(address(stEth), 5 ether); - vm.stopPrank(); - - - assertApproxEqAbs(etherFiRestakerInstance.getRestakedAmount(address(stEth)), currentStEthRestakedAmount + 5 ether, 2 wei); - } - - function test_queueWithdrawals_1() public returns (bytes32[] memory) { - test_restake_stEth(); - - vm.prank(etherfiOperatingAdmin); - return etherFiRestakerInstance.queueWithdrawals(address(stEth), 5 ether); - } - - function test_queueWithdrawals_2() public returns (bytes32[] memory) { - test_restake_stEth(); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - IStrategy[] memory strategies = new IStrategy[](1); - strategies[0] = etherFiRestakerInstance.getEigenLayerRestakingStrategy(address(stEth)); - uint256[] memory shares = new uint256[](1); - shares[0] = eigenLayerStrategyManager.stakerStrategyShares(address(etherFiRestakerInstance), strategies[0]); + // vm.startPrank(alice); + // uint256 stEthBalance = stEth.balanceOf(address(etherFiRestakerInstance)); + // uint256[] memory reqIds = etherFiRestakerInstance.stEthRequestWithdrawal(stEthBalance); + // vm.stopPrank(); - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategies, - shares: shares, - withdrawer: address(etherFiRestakerInstance) - }); - - vm.prank(etherfiOperatingAdmin); - return etherFiRestakerInstance.queueWithdrawals(params); - } + // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 2 wei); + // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), amount, 2 wei); + // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); - function test_completeQueuedWithdrawals_1() public { - bytes32[] memory withdrawalRoots = test_queueWithdrawals_1(); - assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + // bytes32 FINALIZE_ROLE = etherFiRestakerInstance.lidoWithdrawalQueue().FINALIZE_ROLE(); + // address finalize_role = etherFiRestakerInstance.lidoWithdrawalQueue().getRoleMember(FINALIZE_ROLE, 0); - vm.startPrank(etherfiOperatingAdmin); - // It won't complete the withdrawal because the withdrawal is still pending - etherFiRestakerInstance.completeQueuedWithdrawals(1000); - assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); - assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 5 ether, 2 wei); + // // The redemption is approved by the Lido + // vm.startPrank(finalize_role); + // uint256 currentRate = stEth.getTotalPooledEther() * 1e27 / stEth.getTotalShares(); + // (uint256 ethToLock, uint256 sharesToBurn) = etherFiRestakerInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate); + // etherFiRestakerInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate); + // vm.stopPrank(); - vm.roll(block.number + 50400); + // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 2 wei); + // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), amount, 2 wei); + // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); - etherFiRestakerInstance.completeQueuedWithdrawals(1000); - assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); - assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 0, 2 wei); - vm.stopPrank(); - } - - function test_completeQueuedWithdrawals_2() public { - bytes32[] memory withdrawalRoots1 = test_queueWithdrawals_1(); - - vm.roll(block.number + 50400 / 2); - - bytes32[] memory withdrawalRoots2 = test_queueWithdrawals_1(); - - assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); - assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - - vm.roll(block.number + 50400 / 2); + // // The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract + // vm.startPrank(alice); + // uint256 lastCheckPointIndex = etherFiRestakerInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); + // uint256[] memory hints = etherFiRestakerInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); + // etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); - // The first withdrawal is completed - // But, the second withdrawal is still pending - // Therefore, `completeQueuedWithdrawals` will not complete the second withdrawal - vm.startPrank(etherfiOperatingAdmin); - etherFiRestakerInstance.completeQueuedWithdrawals(1000); + // // the cycle completes + // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0, 2 wei); + // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), 0, 2 wei); + // assertApproxEqAbs(address(etherFiRestakerInstance).balance, 0, 2); - assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); - assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); + // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); + // assertApproxEqAbs(address(liquidityPoolInstance).balance, lpBalance + amount, 2 wei); + // } - vm.roll(block.number + 50400 / 2); + // function test_restake_stEth() public { + // uint256 currentStEthRestakedAmount = etherFiRestakerInstance.getRestakedAmount(address(stEth)); - etherFiRestakerInstance.completeQueuedWithdrawals(1000); - assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); - assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - vm.stopPrank(); - } - - function test_delegate_to() public { - _deposit_stEth(10 ether); + // _deposit_stEth(10 ether); - ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ - signature: hex"", - expiry: 0 - }); + // vm.startPrank(alice); + // etherFiRestakerInstance.depositIntoStrategy(address(stEth), 5 ether); + // vm.stopPrank(); - vm.startPrank(etherfiOperatingAdmin); - etherFiRestakerInstance.delegateTo(avsOperator, signature, 0x0); - etherFiRestakerInstance.depositIntoStrategy(address(stEth), 5 ether); - vm.stopPrank(); - } - function test_undelegate() public { - test_delegate_to(); + // assertApproxEqAbs(etherFiRestakerInstance.getRestakedAmount(address(stEth)), currentStEthRestakedAmount + 5 ether, 2 wei); + // } - vm.prank(etherfiOperatingAdmin); - etherFiRestakerInstance.undelegate(); - } + // function test_queueWithdrawals_1() public returns (bytes32[] memory) { + // test_restake_stEth(); - // - function test_change_operator() public { - test_delegate_to(); + // vm.prank(etherfiOperatingAdmin); + // return etherFiRestakerInstance.queueWithdrawals(address(stEth), 5 ether); + // } - ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ - signature: hex"", - expiry: 0 - }); + // function test_queueWithdrawals_2() public returns (bytes32[] memory) { + // test_restake_stEth(); - vm.startPrank(etherfiOperatingAdmin); - vm.expectRevert("DelegationManager._delegate: staker is already actively delegated"); - etherFiRestakerInstance.delegateTo(avsOperator2, signature, 0x0); - vm.stopPrank(); - } -} \ No newline at end of file + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + // IStrategy[] memory strategies = new IStrategy[](1); + // strategies[0] = etherFiRestakerInstance.getEigenLayerRestakingStrategy(address(stEth)); + // uint256[] memory shares = new uint256[](1); + // shares[0] = eigenLayerStrategyManager.stakerStrategyShares(address(etherFiRestakerInstance), strategies[0]); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: strategies, + // shares: shares, + // withdrawer: address(etherFiRestakerInstance) + // }); + + // vm.prank(etherfiOperatingAdmin); + // return etherFiRestakerInstance.queueWithdrawals(params); + // } + + // function test_completeQueuedWithdrawals_1() public { + // bytes32[] memory withdrawalRoots = test_queueWithdrawals_1(); + // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + + // vm.startPrank(etherfiOperatingAdmin); + // // It won't complete the withdrawal because the withdrawal is still pending + // etherFiRestakerInstance.completeQueuedWithdrawals(1000); + // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 5 ether, 2 wei); + + // vm.roll(block.number + 50400); + + // etherFiRestakerInstance.completeQueuedWithdrawals(1000); + // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 0, 2 wei); + // vm.stopPrank(); + // } + + // function test_completeQueuedWithdrawals_2() public { + // bytes32[] memory withdrawalRoots1 = test_queueWithdrawals_1(); + + // vm.roll(block.number + 50400 / 2); + + // bytes32[] memory withdrawalRoots2 = test_queueWithdrawals_1(); + + // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); + // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); + + // vm.roll(block.number + 50400 / 2); + + // // The first withdrawal is completed + // // But, the second withdrawal is still pending + // // Therefore, `completeQueuedWithdrawals` will not complete the second withdrawal + // vm.startPrank(etherfiOperatingAdmin); + // etherFiRestakerInstance.completeQueuedWithdrawals(1000); + + // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); + // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); + + // vm.roll(block.number + 50400 / 2); + + // etherFiRestakerInstance.completeQueuedWithdrawals(1000); + // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); + // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); + // vm.stopPrank(); + // } + + // function test_delegate_to() public { + // _deposit_stEth(10 ether); + + // ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ + // signature: hex"", + // expiry: 0 + // }); + + // vm.startPrank(etherfiOperatingAdmin); + // etherFiRestakerInstance.delegateTo(avsOperator, signature, 0x0); + // etherFiRestakerInstance.depositIntoStrategy(address(stEth), 5 ether); + // vm.stopPrank(); + // } + + // function test_undelegate() public { + // test_delegate_to(); + + // vm.prank(etherfiOperatingAdmin); + // etherFiRestakerInstance.undelegate(); + // } + + // // + // function test_change_operator() public { + // test_delegate_to(); + + // ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ + // signature: hex"", + // expiry: 0 + // }); + + // vm.startPrank(etherfiOperatingAdmin); + // vm.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + // etherFiRestakerInstance.delegateTo(avsOperator2, signature, 0x0); + // vm.stopPrank(); + // } +} diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 137494e21..8c0957641 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -420,7 +420,7 @@ contract TestSetup is Test { etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); - etherFiRestakerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance)); + etherFiRestakerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance), address(0x0)); etherFiRestakerInstance.updateAdmin(alice, true); liquifierInstance.initializeOnUpgrade(address(etherFiRestakerInstance)); @@ -1378,10 +1378,10 @@ contract TestSetup is Test { vm.startPrank(alice); uint256 lastCheckPointIndex = liquifierInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); uint256[] memory hints = liquifierInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); - etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); + // etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); // The ether.fi admin withdraws the ETH from the liquifier contract to the liquidity pool contract - etherFiRestakerInstance.withdrawEther(); + // etherFiRestakerInstance.withdrawEther(); vm.stopPrank(); } @@ -1511,4 +1511,4 @@ contract TestSetup is Test { string memory output_path = string.concat(string("./release/logs/txns/"), string.concat(prefix, string(".json"))); // releast/logs/$(block_number)_{$(block_timestamp)}json stdJson.write(output, output_path); } -} \ No newline at end of file +} From 823896c72bdf3a9488a9c0895d9a0ed6b4c3b6c2 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 14 Nov 2024 19:37:11 -0600 Subject: [PATCH 2/6] updating tests and improving read APIs --- src/EtherFiRestakeManager.sol | 147 +++++++++---- src/EtherFiRestaker.sol | 152 ++++--------- test/EtherFiRestaker.t.sol | 400 +++++++++++++++++++--------------- test/TestSetup.sol | 46 ++-- 4 files changed, 401 insertions(+), 344 deletions(-) diff --git a/src/EtherFiRestakeManager.sol b/src/EtherFiRestakeManager.sol index b58347b46..1488d6a1f 100644 --- a/src/EtherFiRestakeManager.sol +++ b/src/EtherFiRestakeManager.sol @@ -6,32 +6,28 @@ import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "./Liquifier.sol"; import "./EtherFiRestaker.sol"; +import "./RoleRegistry.sol"; -contract EtherFiRestakeManager is - Initializable, - OwnableUpgradeable, - PausableUpgradeable, - UUPSUpgradeable -{ +contract EtherFiRestakeManager is Initializable, OwnableUpgradeable, UUPSUpgradeable { LiquidityPool public liquidityPool; Liquifier public liquifier; + RoleRegistry public roleRegistry; ILidoWithdrawalQueue public lidoWithdrawalQueue; ILido public lido; - mapping(address => bool) public pausers; - mapping(address => bool) public admins; - UpgradeableBeacon public upgradableBeacon; uint256 public nextAvsOperatorId; mapping(uint256 => EtherFiRestaker) public etherFiRestaker; + bytes32 public constant RESTAKING_MANAGER_ADMIN_ROLE = keccak256("RESTAKING_MANAGER_ADMIN_ROLE"); + event QueuedStEthWithdrawals(uint256[] _reqIds); event CompletedStEthQueuedWithdrawals(uint256[] _reqIds); event CreatedEtherFiRestaker(uint256 indexed id, address etherFiRestaker); - error IncorrectCaller(); error IncorrectAmount(); + error IncorrectRole(); error NotEnoughBalance(); /// @custom:oz-upgrades-unsafe-allow constructor @@ -39,18 +35,25 @@ contract EtherFiRestakeManager is _disableInitializers(); } - function initialize(address _liquidityPool, address _liquifier) initializer external { + function initialize(address _liquidityPool, address _liquifier, address _roleRegistry) initializer external { __Ownable_init(); - __Pausable_init(); __UUPSUpgradeable_init(); + nextAvsOperatorId = 1; + upgradableBeacon = new UpgradeableBeacon(address(new EtherFiRestaker())); liquidityPool = LiquidityPool(payable(_liquidityPool)); liquifier = Liquifier(payable(_liquifier)); + roleRegistry = RoleRegistry(_roleRegistry); lido = liquifier.lido(); lidoWithdrawalQueue = liquifier.lidoWithdrawalQueue(); } + + function upgradeEtherFiRestaker(address _newImplementation) public onlyOwner { + upgradableBeacon.upgradeTo(_newImplementation); + } + function instantiateEtherFiRestaker(uint256 _nums) external onlyOwner returns (uint256[] memory _ids) { _ids = new uint256[](_nums); for (uint256 i = 0; i < _nums; i++) { @@ -75,52 +78,91 @@ contract EtherFiRestakeManager is // | EigenLayer Restaking | // |--------------------------------------------------------------------------------------------| + /// @notice delegate to an AVS operator for a `EtherFiRestaker` instance by index + /// @param index `EtherFiRestaker` instance to call `delegate` on function delegateTo(uint256 index, address operator, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) external { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + return etherFiRestaker[index].delegateTo(operator, approverSignatureAndExpiry, approverSalt); } + /// @notice undelegate from the current AVS operator & un-restake all + /// @param index `EtherFiRestaker` instance to call `undelegate` on function undelegate(uint256 index) external returns (bytes32[] memory) { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + return etherFiRestaker[index].undelegate(); } + /// @notice deposit the token in holding into the restaking strategy + /// @param index `EtherFiRestaker` instance to deposit from function depositIntoStrategy( uint256 index, address token, uint256 amount ) external returns (uint256) { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + IERC20(token).transfer(address(etherFiRestaker[index]), amount); return etherFiRestaker[index].depositIntoStrategy(token); } + /// @notice queue withdrawals for un-restaking the token + /// Made easy for operators + /// @param index `EtherFiRestaker` instance to withdraw from + /// @param token the token to withdraw + /// @param amount the amount of token to withdraw function queueWithdrawals( uint256 index, address token, uint256 amount ) external returns (bytes32[] memory) { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + return etherFiRestaker[index].queueWithdrawals(token, amount); } + /// Advanced version + /// @notice queue withdrawals with custom parameters for un-restaking multiple tokens + /// @param index `EtherFiRestaker` instance to withdraw from + /// @param queuedWithdrawalParams Array of withdrawal parameters including strategies and share amounts function queueWithdrawalsAdvanced( uint256 index, IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams ) external returns (bytes32[] memory) { - return etherFiRestaker[index].queueWithdrawals(queuedWithdrawalParams); + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + + return etherFiRestaker[index].queueWithdrawalsAdvanced(queuedWithdrawalParams); } + /// @notice Complete the queued withdrawals that are ready to be withdrawn + /// @param index `EtherFiRestaker` instance to call `completeQueuedWithdrawals` on + /// @param max_cnt the maximum number of withdrawals to complete function completeQueuedWithdrawals( uint256 index, uint256 max_cnt ) external { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + etherFiRestaker[index].completeQueuedWithdrawals(max_cnt); } + /// Advanced version + /// @notice Used to complete the specified `queuedWithdrawals`. The function caller must match `queuedWithdrawals[...].withdrawer` + /// @param index `EtherFiRestaker` instance to call `completeQueuedWithdrawals` on + /// @param _queuedWithdrawals The QueuedWithdrawals to complete. + /// @param _tokens Array of tokens for each QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single array. + /// @param _middlewareTimesIndexes One index to reference per QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single index. + /// @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw` function completeQueuedWithdrawalsAdvanced( uint256 index, IDelegationManager.Withdrawal[] memory _queuedWithdrawals, IERC20[][] memory _tokens, uint256[] memory _middlewareTimesIndexes ) external { - etherFiRestaker[index].completeQueuedWithdrawals( + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + + etherFiRestaker[index].completeQueuedWithdrawalsAdvanced( _queuedWithdrawals, _tokens, _middlewareTimesIndexes @@ -131,16 +173,18 @@ contract EtherFiRestakeManager is // | Handling Lido's stETH | // |--------------------------------------------------------------------------------------------| - /// Initiate the redemption of stETH for ETH - /// @notice Request for all stETH holdings - function stEthRequestWithdrawal() external onlyAdmin returns (uint256[] memory) { + /// @notice Initiate the redemption of stETH for ETH + function stEthRequestWithdrawal() external returns (uint256[] memory) { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + uint256 amount = lido.balanceOf(address(this)); return stEthRequestWithdrawal(amount); } /// @notice Request for a specific amount of stETH holdings /// @param _amount the amount of stETH to request - function stEthRequestWithdrawal(uint256 _amount) public onlyAdmin returns (uint256[] memory) { + function stEthRequestWithdrawal(uint256 _amount) public returns (uint256[] memory) { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); if (_amount < lidoWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()) revert IncorrectAmount(); if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance(); @@ -161,8 +205,9 @@ contract EtherFiRestakeManager is /// @notice Claim a batch of withdrawal requests if they are finalized sending the ETH to the this contract back /// @param _requestIds array of request ids to claim /// @param _hints checkpoint hint for each id. Can be obtained with `findCheckpointHints()` - function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external onlyAdmin { - uint256 balance = address(this).balance; + function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + lidoWithdrawalQueue.claimWithdrawals(_requestIds, _hints); withdrawEther(); @@ -170,8 +215,10 @@ contract EtherFiRestakeManager is emit CompletedStEthQueuedWithdrawals(_requestIds); } - // Send the ETH back to the liquidity pool - function withdrawEther() public onlyAdmin { + /// @notice Sends the ETH in this contract to the liquidity pool + function withdrawEther() public { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + uint256 amountToLiquidityPool = address(this).balance; (bool sent, ) = payable(address(liquidityPool)).call{value: amountToLiquidityPool, gas: 20000}(""); require(sent, "ETH_SEND_TO_LIQUIDITY_POOL_FAILED"); @@ -181,35 +228,49 @@ contract EtherFiRestakeManager is // | VIEW functions | // |--------------------------------------------------------------------------------------------| - /// @notice Returns the total stETH {staked, unstaked} - function getTotalPooledStETH() external view returns (uint256 amount){ + /// @notice The total amount in wei of assets controlled by the `EtherFiRestakingManager` and `EtherFiRestaker` instances + /// @dev Only considers stETH. Will need modification to support additional tokens + function getTotalPooledEther() external view returns (uint256 amount){ uint256 amount = lido.balanceOf(address(this)); - for (uint256 i = 0; i < nextAvsOperatorId; i++) { - amount += etherFiRestaker[i].getRestakedAmount(); + amount += getEthAmountPendingForRedemption(address(lido)); + for (uint256 i = 1; i < nextAvsOperatorId; i++) { + amount += etherFiRestaker[i].getTotalPooledEther(); } return amount; } - receive() external payable {} - - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - - function _requireAdmin() internal view virtual { - if (!(admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); + /// @notice The assets controlled by the manager split between the 4 states + /// - restaked in Eigenlayer, + /// - pending for un-restaking from Eigenlayer + /// - non-restaked & held by this contract + /// - non-restaked & pending in redemption for ETH + /// @dev Only considers stETH. Will need modification to support additional tokens + function getTotalPooledEtherSplits() public view returns (uint256 holding, uint256 pendingForWithdrawals, uint256 restaked, uint256 unrestaking) { + holding = lido.balanceOf(address(this)); + pendingForWithdrawals = getEthAmountPendingForRedemption(address(lido)); + for (uint256 i = 1; i < nextAvsOperatorId; i++) { + (uint256 restakedInInstance, uint256 unrestakedInInstance) = etherFiRestaker[i].getTotalPooledEtherSplits(); + restaked += restakedInInstance; + unrestaking += unrestakedInInstance; + } } - function _requirePauser() internal view virtual { - if (!(pausers[msg.sender] || admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); + function getEthAmountPendingForRedemption(address _token) public view returns (uint256) { + uint256 total = 0; + if (_token == address(lido)) { + uint256[] memory stEthWithdrawalRequestIds = lidoWithdrawalQueue.getWithdrawalRequests(address(this)); + ILidoWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = lidoWithdrawalQueue.getWithdrawalStatus(stEthWithdrawalRequestIds); + for (uint256 i = 0; i < statuses.length; i++) { + require(statuses[i].owner == address(this), "Not the owner"); + require(!statuses[i].isClaimed, "Already claimed"); + total += statuses[i].amountOfStETH; + } + } + return total; } - /* MODIFIER */ - modifier onlyAdmin() { - _requireAdmin(); - _; - } + receive() external payable {} + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - modifier onlyPauser() { - _requirePauser(); - _; - } } diff --git a/src/EtherFiRestaker.sol b/src/EtherFiRestaker.sol index 91aa8be93..dfa271115 100644 --- a/src/EtherFiRestaker.sol +++ b/src/EtherFiRestaker.sol @@ -1,12 +1,6 @@ /// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -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/PausableUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -16,7 +10,7 @@ import "./LiquidityPool.sol"; import "./eigenlayer-interfaces/IStrategyManager.sol"; import "./eigenlayer-interfaces/IDelegationManager.sol"; -contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable { +contract EtherFiRestaker is Initializable { using SafeERC20 for IERC20; using EnumerableSet for EnumerableSet.Bytes32Set; @@ -29,33 +23,17 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, LiquidityPool public liquidityPool; Liquifier public liquifier; address public etherFiRestakeManager; - ILidoWithdrawalQueue public lidoWithdrawalQueue; ILido public lido; IDelegationManager public eigenLayerDelegationManager; IStrategyManager public eigenLayerStrategyManager; - mapping(address => bool) public pausers; - mapping(address => bool) public admins; - mapping(address => TokenInfo) public tokenInfos; EnumerableSet.Bytes32Set private withdrawalRootsSet; mapping(bytes32 => IDelegationManager.Withdrawal) public withdrawalRootToWithdrawal; - - event QueuedStEthWithdrawals(uint256[] _reqIds); - event CompletedStEthQueuedWithdrawals(uint256[] _reqIds); event CompletedQueuedWithdrawal(bytes32 _withdrawalRoot); - error NotEnoughBalance(); - error IncorrectAmount(); - error StrategyShareNotEnough(); - error EthTransferFailed(); - error AlreadyRegistered(); - error NotRegistered(); - error WrongOutput(); - error IncorrectCaller(); - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -63,16 +41,12 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// @notice initialize to set variables on deployment function initialize(address _liquidityPool, address _liquifier, address _manager) initializer external { - __Ownable_init(); - __Pausable_init(); - __UUPSUpgradeable_init(); liquidityPool = LiquidityPool(payable(_liquidityPool)); liquifier = Liquifier(payable(_liquifier)); etherFiRestakeManager = _manager; lido = liquifier.lido(); - lidoWithdrawalQueue = liquifier.lidoWithdrawalQueue(); eigenLayerStrategyManager = liquifier.eigenLayerStrategyManager(); eigenLayerDelegationManager = liquifier.eigenLayerDelegationManager(); @@ -84,25 +58,23 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, }); } - receive() external payable {} - // |--------------------------------------------------------------------------------------------| // | EigenLayer Restaking | // |--------------------------------------------------------------------------------------------| - // delegate to an AVS operator + /// @notice delegate to an AVS operator function delegateTo(address operator, IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) external managerOnly { eigenLayerDelegationManager.delegateTo(operator, approverSignatureAndExpiry, approverSalt); } - // undelegate from the current AVS operator & un-restake all + /// @notice undelegate from the current AVS operator & un-restake all function undelegate() external managerOnly returns (bytes32[] memory) { // Un-restake all assets // Currently, only stETH is supported TokenInfo memory info = tokenInfos[address(lido)]; uint256 shares = eigenLayerStrategyManager.stakerStrategyShares(address(this), info.elStrategy); - _queueWithdrawlsByShares(address(lido), shares); + _queueWithdrawalsByShares(address(lido), shares); bytes32[] memory withdrawalRoots = eigenLayerDelegationManager.undelegate(address(this)); assert(withdrawalRoots.length == 0); @@ -110,7 +82,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, return withdrawalRoots; } - // deposit the token in holding into the restaking strategy + /// @notice deposit the balance of the token in into the restaking strategy function depositIntoStrategy(address token) external managerOnly returns (uint256) { // using `balanceOf` instead of passing the amount param from `EtherFiRestakeManager.depositIntoStrategy` to avoid 1-2 wei corner case on stETH transfers uint256 amount = IERC20(token).balanceOf(address(this)); @@ -122,17 +94,19 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, return shares; } - /// queue withdrawals for un-restaking the token + /// @notice queue withdrawals for un-restaking the token /// Made easy for operators /// @param token the token to withdraw /// @param amount the amount of token to withdraw function queueWithdrawals(address token, uint256 amount) public managerOnly returns (bytes32[] memory) { uint256 shares = getEigenLayerRestakingStrategy(token).underlyingToSharesView(amount); - return _queueWithdrawlsByShares(token, shares); + return _queueWithdrawalsByShares(token, shares); } /// Advanced version - function queueWithdrawals(IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams) public managerOnly returns (bytes32[] memory) { + /// @notice queue withdrawals with custom parameters for un-restaking multiple tokens + /// @param queuedWithdrawalParams Array of withdrawal parameters including strategies, share amounts, and withdrawer + function queueWithdrawalsAdvanced(IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams) public managerOnly returns (bytes32[] memory) { uint256 currentNonce = eigenLayerDelegationManager.cumulativeWithdrawalsQueued(address(this)); bytes32[] memory withdrawalRoots = eigenLayerDelegationManager.queueWithdrawals(queuedWithdrawalParams); @@ -207,7 +181,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, mstore(_middlewareTimesIndexes, cnt) } - completeQueuedWithdrawals(_queuedWithdrawals, _tokens, _middlewareTimesIndexes); + completeQueuedWithdrawalsAdvanced(_queuedWithdrawals, _tokens, _middlewareTimesIndexes); } /// Advanced version @@ -216,7 +190,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// @param _tokens Array of tokens for each QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single array. /// @param _middlewareTimesIndexes One index to reference per QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single index. /// @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw` - function completeQueuedWithdrawals(IDelegationManager.Withdrawal[] memory _queuedWithdrawals, IERC20[][] memory _tokens, uint256[] memory _middlewareTimesIndexes) public managerOnly { + function completeQueuedWithdrawalsAdvanced(IDelegationManager.Withdrawal[] memory _queuedWithdrawals, IERC20[][] memory _tokens, uint256[] memory _middlewareTimesIndexes) public managerOnly { uint256 num = _queuedWithdrawals.length; bool[] memory receiveAsTokens = new bool[](num); for (uint256 i = 0; i < num; i++) { @@ -233,7 +207,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// transfer tokens back to manager for (uint256 i = 0; i < _queuedWithdrawals.length; ++i) { - for (uint256 j = 0; j < _tokens[i].length; ++i) { + for (uint256 j = 0; j < _tokens[i].length; ++j) { _tokens[i][j].transfer(etherFiRestakeManager, _tokens[i][j].balanceOf(address(this))); } } @@ -243,29 +217,39 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, // | VIEW functions | // |--------------------------------------------------------------------------------------------| - /// Enumerate the pending withdrawal roots - // used in complete queued withdrawal function + /// @notice Enumerate the pending withdrawal roots function pendingWithdrawalRoots() public view returns (bytes32[] memory) { return withdrawalRootsSet.values(); } - /// Check if a withdrawal is pending for a given withdrawal root - // not directly used in withdrawal logic, seems like it could be useful + /// @notice Check if a withdrawal is pending for a given withdrawal root function isPendingWithdrawal(bytes32 _withdrawalRoot) external view returns (bool) { return withdrawalRootsSet.contains(_withdrawalRoot); } - // Returns the amount that this contract has restaked denominated in Ether - function getRestakedAmount() external view returns (uint256 amount) { - return getTotalPooledEther(address(lido)); + /// @notice The total amount of assets controlled by this contract in wei + /// @dev Only considers stETH. Will need modification to support additional tokens + function getTotalPooledEther() public view returns (uint256) { + (uint256 restaked, uint256 unrestaking) = getTotalPooledEtherSplits(address(lido)); + return restaked + unrestaking; } - function getTotalPooledEther(address _token) public view returns (uint256) { - (uint256 restaked, uint256 unrestaking, uint256 holding, uint256 pendingForWithdrawals) = getTotalPooledEtherSplits(_token); - return restaked + unrestaking + holding + pendingForWithdrawals; + /// @notice The assets held by this contract in Eigenlayer split between restaked and pending for un-restaking + /// @dev Only considers stETH. Will need modification to support additional tokens + function getTotalPooledEtherSplits() public view returns (uint256 restaked, uint256 unrestaking) { + (restaked, unrestaking) = getTotalPooledEtherSplits(address(lido)); + return (restaked, unrestaking); } - - // used in the getRestakedAmount function + + function getTotalPooledEtherSplits(address _token) public view returns (uint256 restaked, uint256 unrestaking) { + TokenInfo memory info = tokenInfos[_token]; + if (info.elStrategy != IStrategy(address(0))) { + uint256 restakedTokenAmount = getRestakedAmount(_token); + restaked = liquifier.quoteByFairValue(_token, restakedTokenAmount); /// restaked & pending for withdrawals + unrestaking = getEthAmountInEigenLayerPendingForWithdrawals(_token); + } + } + function getRestakedAmount(address _token) public view returns (uint256) { TokenInfo memory info = tokenInfos[_token]; uint256 shares = eigenLayerStrategyManager.stakerStrategyShares(address(this), info.elStrategy); @@ -277,21 +261,6 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, return tokenInfos[_token].elStrategy; } - /// each asset in holdings can have 3 states: - /// - in Eigenlayer, either restaked or pending for un-restaking - /// - non-restaked & held by this contract - /// - non-restaked & not held by this contract & pending in redemption for ETH - function getTotalPooledEtherSplits(address _token) public view returns (uint256 restaked, uint256 unrestaking, uint256 holding, uint256 pendingForWithdrawals) { - TokenInfo memory info = tokenInfos[_token]; - if (info.elStrategy != IStrategy(address(0))) { - uint256 restakedTokenAmount = getRestakedAmount(_token); - restaked = liquifier.quoteByFairValue(_token, restakedTokenAmount); /// restaked & pending for withdrawals - unrestaking = getEthAmountInEigenLayerPendingForWithdrawals(_token); - } - holding = liquifier.quoteByFairValue(_token, IERC20(_token).balanceOf(address(this))); /// eth value for erc20 holdings - pendingForWithdrawals = getEthAmountPendingForRedemption(_token); - } - function getEthAmountInEigenLayerPendingForWithdrawals(address _token) public view returns (uint256) { TokenInfo memory info = tokenInfos[_token]; if (info.elStrategy == IStrategy(address(0))) return 0; @@ -299,40 +268,8 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, return amount; } - function getEthAmountPendingForRedemption(address _token) public view returns (uint256) { - uint256 total = 0; - if (_token == address(lido)) { - uint256[] memory stEthWithdrawalRequestIds = lidoWithdrawalQueue.getWithdrawalRequests(address(this)); - ILidoWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = lidoWithdrawalQueue.getWithdrawalStatus(stEthWithdrawalRequestIds); - for (uint256 i = 0; i < statuses.length; i++) { - require(statuses[i].owner == address(this), "Not the owner"); - require(!statuses[i].isClaimed, "Already claimed"); - total += statuses[i].amountOfStETH; - } - } - return total; - } - - function updateAdmin(address _address, bool _isAdmin) external onlyOwner { - admins[_address] = _isAdmin; - } - - function updatePauser(address _address, bool _isPauser) external managerOnly { - pausers[_address] = _isPauser; - } - - // Pauses the contract - function pauseContract() external onlyPauser { - _pause(); - } - - // Unpauses the contract - function unPauseContract() external managerOnly { - _unpause(); - } - // INTERNAL functions - function _queueWithdrawlsByShares(address token, uint256 shares) internal returns (bytes32[] memory) { + function _queueWithdrawalsByShares(address token, uint256 shares) internal returns (bytes32[] memory) { IStrategy strategy = tokenInfos[token].elStrategy; IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); IStrategy[] memory strategies = new IStrategy[](1); @@ -346,27 +283,14 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, withdrawer: address(this) }); - return queueWithdrawals(params); + return queueWithdrawalsAdvanced(params); } function _min(uint256 _a, uint256 _b) internal pure returns (uint256) { return (_a > _b) ? _b : _a; } - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - - function _requireAdmin() internal view virtual { - if (!(admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); - } - - function _requirePauser() internal view virtual { - if (!(pausers[msg.sender] || admins[msg.sender] || msg.sender == owner())) revert IncorrectCaller(); - } - - modifier onlyPauser() { - _requirePauser(); - _; - } + receive() external payable {} modifier managerOnly() { require(msg.sender == etherFiRestakeManager, "NOT_MANAGER"); diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol index 5f3a7e2ce..26695d8cd 100644 --- a/test/EtherFiRestaker.t.sol +++ b/test/EtherFiRestaker.t.sol @@ -25,204 +25,264 @@ contract EtherFiRestakerTest is TestSetup { avsOperator = 0x5ACCC90436492F24E6aF278569691e2c942A676d; // EigenYields avsOperator2 = 0xfB487f216CA24162119C0C6Ae015d680D7569C2f; - etherfiOperatingAdmin = alice; // + etherfiOperatingAdmin = alice; - vm.prank(owner); + vm.startPrank(owner); liquifierInstance.updateQuoteStEthWithCurve(false); + roleRegistryInstance.grantRole(etherFiRestakeManagerInstance.RESTAKING_MANAGER_ADMIN_ROLE(), etherfiOperatingAdmin); + etherFiRestakeManagerInstance.instantiateEtherFiRestaker(3); + vm.stopPrank(); + } - // function _deposit_stEth(uint256 _amount) internal { - // uint256 restakerTvl = etherFiRestakerInstance.getTotalPooledEther(); - // uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - // uint256 lpBalance = address(liquidityPoolInstance).balance; - // uint256 aliceStEthBalance = stEth.balanceOf(alice); - // uint256 aliceEEthBalance = eETHInstance.balanceOf(alice); + function _deposit_stEth(uint256 _amount) internal { + uint256 restakerTvl = etherFiRestakeManagerInstance.getTotalPooledEther(); + uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); + uint256 lpBalance = address(liquidityPoolInstance).balance; + uint256 aliceStEthBalance = stEth.balanceOf(alice); + uint256 aliceEEthBalance = eETHInstance.balanceOf(alice); - // vm.deal(alice, _amount); - // vm.startPrank(alice); - // stEth.submit{value: _amount}(address(0)); + vm.deal(alice, _amount); + vm.startPrank(alice); + stEth.submit{value: _amount}(address(0)); - // ILiquidityPool.PermitInput memory permitInput = createPermitInput(2, address(liquifierInstance), _amount, stEth.nonces(alice), 2**256 - 1, stEth.DOMAIN_SEPARATOR()); - // ILiquifier.PermitInput memory permitInput2 = ILiquifier.PermitInput({value: permitInput.value, deadline: permitInput.deadline, v: permitInput.v, r: permitInput.r, s: permitInput.s}); - // liquifierInstance.depositWithERC20WithPermit(address(stEth), _amount, address(0), permitInput2); + ILiquidityPool.PermitInput memory permitInput = createPermitInput(2, address(liquifierInstance), _amount, stEth.nonces(alice), 2**256 - 1, stEth.DOMAIN_SEPARATOR()); + ILiquifier.PermitInput memory permitInput2 = ILiquifier.PermitInput({value: permitInput.value, deadline: permitInput.deadline, v: permitInput.v, r: permitInput.r, s: permitInput.s}); + liquifierInstance.depositWithERC20WithPermit(address(stEth), _amount, address(0), permitInput2); + + + // Alice has 10 ether eETH + // Total eETH TVL is 10 ether + assertApproxEqAbs(stEth.balanceOf(alice), aliceStEthBalance, 1 wei); + assertApproxEqAbs(eETHInstance.balanceOf(alice), aliceEEthBalance + _amount, 1 wei); + assertApproxEqAbs(etherFiRestakeManagerInstance.getTotalPooledEther(), restakerTvl + _amount, 1 wei); + assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + _amount, 1 wei); + vm.stopPrank(); + } + + function test_withdrawal_of_non_restaked_stEth() public { + uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); + uint256 lpBalance = address(liquidityPoolInstance).balance; + + uint256 amount = 10 ether; + + // deposit stETH into liquifier + _deposit_stEth(amount); + + assertEq(etherFiRestakeManagerInstance.getEthAmountPendingForRedemption(address(stEth)), 0); + + vm.startPrank(alice); + uint256 stEthBalance = stEth.balanceOf(address(etherFiRestakeManagerInstance)); + uint256[] memory reqIds = etherFiRestakeManagerInstance.stEthRequestWithdrawal(stEthBalance); + vm.stopPrank(); + + assertApproxEqAbs(etherFiRestakeManagerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 3 wei); + assertApproxEqAbs(etherFiRestakeManagerInstance.getTotalPooledEther(), amount, 3 wei); + assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 3 wei); + + bytes32 FINALIZE_ROLE = etherFiRestakeManagerInstance.lidoWithdrawalQueue().FINALIZE_ROLE(); + address finalize_role = etherFiRestakeManagerInstance.lidoWithdrawalQueue().getRoleMember(FINALIZE_ROLE, 0); + + // The redemption is approved by the Lido + vm.startPrank(finalize_role); + uint256 currentRate = stEth.getTotalPooledEther() * 1e27 / stEth.getTotalShares(); + (uint256 ethToLock, uint256 sharesToBurn) = etherFiRestakeManagerInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate); + etherFiRestakeManagerInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate); + vm.stopPrank(); + + assertApproxEqAbs(etherFiRestakeManagerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 3 wei); + assertApproxEqAbs(etherFiRestakeManagerInstance.getTotalPooledEther(), amount, 3 wei); + assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 3 wei); + + // The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract + vm.startPrank(alice); + uint256 lastCheckPointIndex = etherFiRestakeManagerInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); + uint256[] memory hints = etherFiRestakeManagerInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); + etherFiRestakeManagerInstance.stEthClaimWithdrawals(reqIds, hints); + + // the cycle completes + assertApproxEqAbs(etherFiRestakeManagerInstance.getEthAmountPendingForRedemption(address(stEth)), 0, 3 wei); + assertApproxEqAbs(etherFiRestakeManagerInstance.getTotalPooledEther(), 0, 3 wei); + assertApproxEqAbs(address(etherFiRestakeManagerInstance).balance, 0, 2); + + assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 3 wei); + assertApproxEqAbs(address(liquidityPoolInstance).balance, lpBalance + amount, 3 wei); + } + + function test_restake_stEth() public { + (,,uint256 stEthRestakedAmountBefore,) = etherFiRestakeManagerInstance.getTotalPooledEtherSplits(); + + _deposit_stEth(10 ether); + vm.startPrank(alice); + etherFiRestakeManagerInstance.depositIntoStrategy(1, address(stEth), 5 ether); + vm.stopPrank(); - // // Aliice has 10 ether eETH - // // Total eETH TVL is 10 ether - // assertApproxEqAbs(stEth.balanceOf(alice), aliceStEthBalance, 1 wei); - // assertApproxEqAbs(eETHInstance.balanceOf(alice), aliceEEthBalance + _amount, 1 wei); - // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), restakerTvl + _amount, 1 wei); - // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + _amount, 1 wei); - // vm.stopPrank(); - // } + (,,uint256 stEthRestakedAmountAfter,) = etherFiRestakeManagerInstance.getTotalPooledEtherSplits(); - // function test_withdrawal_of_non_restaked_stEth() public { - // uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - // uint256 lpBalance = address(liquidityPoolInstance).balance; + assertApproxEqAbs(stEthRestakedAmountAfter, stEthRestakedAmountBefore + 5 ether, 3 wei); + } + + function test_queueWithdrawals_1() public returns (bytes32[] memory) { + test_restake_stEth(); - // uint256 amount = 10 ether; + vm.prank(etherfiOperatingAdmin); + return etherFiRestakeManagerInstance.queueWithdrawals(1, address(stEth), 5 ether - 2); + } - // _deposit_stEth(amount); + function test_queueWithdrawals_2() public returns (bytes32[] memory) { + test_restake_stEth(); - // assertEq(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0); + address etherFiRestakerInstance = address(etherFiRestakeManagerInstance.etherFiRestaker(1)); - // vm.startPrank(alice); - // uint256 stEthBalance = stEth.balanceOf(address(etherFiRestakerInstance)); - // uint256[] memory reqIds = etherFiRestakerInstance.stEthRequestWithdrawal(stEthBalance); - // vm.stopPrank(); + IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = EtherFiRestaker(payable(etherFiRestakerInstance)).getEigenLayerRestakingStrategy(address(stEth)); + uint256[] memory shares = new uint256[](1); + shares[0] = eigenLayerStrategyManager.stakerStrategyShares(address(etherFiRestakerInstance), strategies[0]); - // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 2 wei); - // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), amount, 2 wei); - // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); + params[0] = IDelegationManager.QueuedWithdrawalParams({ + strategies: strategies, + shares: shares, + withdrawer: address(etherFiRestakerInstance) + }); + + vm.prank(etherfiOperatingAdmin); + return etherFiRestakeManagerInstance.queueWithdrawalsAdvanced(1, params); + } + + function test_completeQueuedWithdrawals_1() public { + bytes32[] memory withdrawalRoots = test_queueWithdrawals_1(); + + + EtherFiRestaker etherFiRestakerInstance = etherFiRestakeManagerInstance.etherFiRestaker(1); + + assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + assertApproxEqAbs(etherFiRestakeManagerInstance.getTotalPooledEther(), 10 ether, 3); + + vm.startPrank(etherfiOperatingAdmin); + // It won't complete the withdrawal because the withdrawal is still pending + etherFiRestakeManagerInstance.completeQueuedWithdrawals(1, 1000); + assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 5 ether, 3 wei); + + vm.roll(block.number + 50400); + + etherFiRestakeManagerInstance.completeQueuedWithdrawals(1, 1000); + assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); + assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 0, 3 wei); + assertApproxEqAbs(etherFiRestakeManagerInstance.getTotalPooledEther(), 10 ether, 4); + vm.stopPrank(); + } + + function test_completeQueuedWithdrawals_2() public { + bytes32[] memory withdrawalRoots1 = test_queueWithdrawals_1(); - // bytes32 FINALIZE_ROLE = etherFiRestakerInstance.lidoWithdrawalQueue().FINALIZE_ROLE(); - // address finalize_role = etherFiRestakerInstance.lidoWithdrawalQueue().getRoleMember(FINALIZE_ROLE, 0); + vm.roll(block.number + 50400 / 2); - // // The redemption is approved by the Lido - // vm.startPrank(finalize_role); - // uint256 currentRate = stEth.getTotalPooledEther() * 1e27 / stEth.getTotalShares(); - // (uint256 ethToLock, uint256 sharesToBurn) = etherFiRestakerInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate); - // etherFiRestakerInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate); - // vm.stopPrank(); + bytes32[] memory withdrawalRoots2 = test_queueWithdrawals_1(); - // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), amount, 2 wei); - // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), amount, 2 wei); - // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); + EtherFiRestaker etherFiRestakerInstance = etherFiRestakeManagerInstance.etherFiRestaker(1); - // // The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract - // vm.startPrank(alice); - // uint256 lastCheckPointIndex = etherFiRestakerInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); - // uint256[] memory hints = etherFiRestakerInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); - // etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); + assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); + assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - // // the cycle completes - // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0, 2 wei); - // assertApproxEqAbs(etherFiRestakerInstance.getTotalPooledEther(), 0, 2 wei); - // assertApproxEqAbs(address(etherFiRestakerInstance).balance, 0, 2); + vm.roll(block.number + 50400 / 2); - // assertApproxEqAbs(liquidityPoolInstance.getTotalPooledEther(), lpTvl + amount, 2 wei); - // assertApproxEqAbs(address(liquidityPoolInstance).balance, lpBalance + amount, 2 wei); - // } + // The first withdrawal is completed + // But, the second withdrawal is still pending + // Therefore, `completeQueuedWithdrawals` will not complete the second withdrawal + vm.startPrank(etherfiOperatingAdmin); + etherFiRestakeManagerInstance.completeQueuedWithdrawals(1, 1000); - // function test_restake_stEth() public { - // uint256 currentStEthRestakedAmount = etherFiRestakerInstance.getRestakedAmount(address(stEth)); + assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); + assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - // _deposit_stEth(10 ether); + vm.roll(block.number + 50400 / 2); - // vm.startPrank(alice); - // etherFiRestakerInstance.depositIntoStrategy(address(stEth), 5 ether); - // vm.stopPrank(); + etherFiRestakeManagerInstance.completeQueuedWithdrawals(1, 1000); + assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); + assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); + vm.stopPrank(); + } + function test_delegate_to() public { + _deposit_stEth(10 ether); - // assertApproxEqAbs(etherFiRestakerInstance.getRestakedAmount(address(stEth)), currentStEthRestakedAmount + 5 ether, 2 wei); - // } + ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ + signature: hex"", + expiry: 0 + }); - // function test_queueWithdrawals_1() public returns (bytes32[] memory) { - // test_restake_stEth(); + vm.startPrank(etherfiOperatingAdmin); + etherFiRestakeManagerInstance.delegateTo(1, avsOperator, signature, 0x0); + etherFiRestakeManagerInstance.depositIntoStrategy(1, address(stEth), 5 ether); + vm.stopPrank(); + } - // vm.prank(etherfiOperatingAdmin); - // return etherFiRestakerInstance.queueWithdrawals(address(stEth), 5 ether); - // } + function test_undelegate() public { + test_delegate_to(); - // function test_queueWithdrawals_2() public returns (bytes32[] memory) { - // test_restake_stEth(); + vm.prank(etherfiOperatingAdmin); + etherFiRestakeManagerInstance.undelegate(1); + } - // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - // IStrategy[] memory strategies = new IStrategy[](1); - // strategies[0] = etherFiRestakerInstance.getEigenLayerRestakingStrategy(address(stEth)); - // uint256[] memory shares = new uint256[](1); - // shares[0] = eigenLayerStrategyManager.stakerStrategyShares(address(etherFiRestakerInstance), strategies[0]); + function test_multi_restakers_multi_states() public { + EtherFiRestaker etherFiRestakerInstance = etherFiRestakeManagerInstance.etherFiRestaker(1); - // params[0] = IDelegationManager.QueuedWithdrawalParams({ - // strategies: strategies, - // shares: shares, - // withdrawer: address(etherFiRestakerInstance) - // }); - - // vm.prank(etherfiOperatingAdmin); - // return etherFiRestakerInstance.queueWithdrawals(params); - // } - - // function test_completeQueuedWithdrawals_1() public { - // bytes32[] memory withdrawalRoots = test_queueWithdrawals_1(); - // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); - - // vm.startPrank(etherfiOperatingAdmin); - // // It won't complete the withdrawal because the withdrawal is still pending - // etherFiRestakerInstance.completeQueuedWithdrawals(1000); - // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); - // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 5 ether, 2 wei); - - // vm.roll(block.number + 50400); - - // etherFiRestakerInstance.completeQueuedWithdrawals(1000); - // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots[0])); - // assertApproxEqAbs(etherFiRestakerInstance.getEthAmountInEigenLayerPendingForWithdrawals(address(stEth)), 0, 2 wei); - // vm.stopPrank(); - // } - - // function test_completeQueuedWithdrawals_2() public { - // bytes32[] memory withdrawalRoots1 = test_queueWithdrawals_1(); - - // vm.roll(block.number + 50400 / 2); - - // bytes32[] memory withdrawalRoots2 = test_queueWithdrawals_1(); - - // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); - // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - - // vm.roll(block.number + 50400 / 2); - - // // The first withdrawal is completed - // // But, the second withdrawal is still pending - // // Therefore, `completeQueuedWithdrawals` will not complete the second withdrawal - // vm.startPrank(etherfiOperatingAdmin); - // etherFiRestakerInstance.completeQueuedWithdrawals(1000); - - // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); - // assertTrue(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - - // vm.roll(block.number + 50400 / 2); - - // etherFiRestakerInstance.completeQueuedWithdrawals(1000); - // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots1[0])); - // assertFalse(etherFiRestakerInstance.isPendingWithdrawal(withdrawalRoots2[0])); - // vm.stopPrank(); - // } - - // function test_delegate_to() public { - // _deposit_stEth(10 ether); - - // ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ - // signature: hex"", - // expiry: 0 - // }); - - // vm.startPrank(etherfiOperatingAdmin); - // etherFiRestakerInstance.delegateTo(avsOperator, signature, 0x0); - // etherFiRestakerInstance.depositIntoStrategy(address(stEth), 5 ether); - // vm.stopPrank(); - // } - - // function test_undelegate() public { - // test_delegate_to(); - - // vm.prank(etherfiOperatingAdmin); - // etherFiRestakerInstance.undelegate(); - // } - - // // - // function test_change_operator() public { - // test_delegate_to(); - - // ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ - // signature: hex"", - // expiry: 0 - // }); - - // vm.startPrank(etherfiOperatingAdmin); - // vm.expectRevert("DelegationManager._delegate: staker is already actively delegated"); - // etherFiRestakerInstance.delegateTo(avsOperator2, signature, 0x0); - // vm.stopPrank(); - // } + test_queueWithdrawals_1(); + + (uint256 holding, uint256 pendingForWithdrawals, uint256 restaked, uint256 unrestaking) = etherFiRestakeManagerInstance.getTotalPooledEtherSplits(); + + assertApproxEqAbs(holding, 5 ether, 3); + assertApproxEqAbs(pendingForWithdrawals, 0, 3); + assertApproxEqAbs(restaked, 0, 3); + assertApproxEqAbs(unrestaking, 5 ether, 3); + + vm.prank(etherfiOperatingAdmin); + etherFiRestakeManagerInstance.stEthRequestWithdrawal(1 ether); + + (holding, pendingForWithdrawals, restaked, unrestaking) = etherFiRestakeManagerInstance.getTotalPooledEtherSplits(); + + assertApproxEqAbs(holding, 4 ether, 3); + assertApproxEqAbs(pendingForWithdrawals, 1 ether, 3); + assertApproxEqAbs(restaked, 0, 3); + assertApproxEqAbs(unrestaking, 5 ether, 3); + + vm.startPrank(alice); + etherFiRestakeManagerInstance.depositIntoStrategy(3, address(stEth), 2 ether); + + (holding, pendingForWithdrawals, restaked, unrestaking) = etherFiRestakeManagerInstance.getTotalPooledEtherSplits(); + + assertApproxEqAbs(holding, 2 ether, 3); + assertApproxEqAbs(pendingForWithdrawals, 1 ether, 3); + assertApproxEqAbs(restaked, 2 ether, 3); + assertApproxEqAbs(unrestaking, 5 ether, 3); + + vm.startPrank(etherfiOperatingAdmin); + vm.roll(block.number + 50400); + etherFiRestakeManagerInstance.completeQueuedWithdrawals(1, 1000); + + (holding, pendingForWithdrawals, restaked, unrestaking) = etherFiRestakeManagerInstance.getTotalPooledEtherSplits(); + + assertApproxEqAbs(holding, 7 ether, 3); + assertApproxEqAbs(pendingForWithdrawals, 1 ether, 3); + assertApproxEqAbs(restaked, 2 ether, 3); + assertApproxEqAbs(unrestaking, 0, 3); + + } + + function test_change_operator() public { + test_delegate_to(); + + ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ + signature: hex"", + expiry: 0 + }); + + vm.startPrank(etherfiOperatingAdmin); + vm.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + etherFiRestakeManagerInstance.delegateTo(1, avsOperator2, signature, 0x0); + vm.stopPrank(); + } } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 8c0957641..620eee367 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -27,7 +27,7 @@ import "../src/Treasury.sol"; import "../src/EtherFiNode.sol"; import "../src/LiquidityPool.sol"; import "../src/Liquifier.sol"; -import "../src/EtherFiRestaker.sol"; +import "../src/EtherFiRestakeManager.sol"; import "../src/EETH.sol"; import "../src/WeETH.sol"; import "../src/MembershipManager.sol"; @@ -47,7 +47,8 @@ import "../src/archive/MembershipManagerV0.sol"; import "../src/EtherFiOracle.sol"; import "../src/EtherFiAdmin.sol"; import "../src/EtherFiTimelock.sol"; - +import "../src/EtherFiRestakeManager.sol"; +import "../src/RoleRegistry.sol"; import "../src/BucketRateLimiter.sol"; contract TestSetup is Test { @@ -56,7 +57,6 @@ contract TestSetup is Test { event Execute(address target, uint256 value, bytes data, bytes32 predecessor, bytes32 salt); event Transaction(address to, uint256 value, bytes data); - uint256 public constant kwei = 10 ** 3; uint256 public slippageLimit = 50; @@ -93,7 +93,8 @@ contract TestSetup is Test { UUPSProxy public BNFTProxy; UUPSProxy public liquidityPoolProxy; UUPSProxy public liquifierProxy; - UUPSProxy public etherFiRestakerProxy; + UUPSProxy public etherFiRestakeManagerProxy; + UUPSProxy public roleRegistryProxy; UUPSProxy public eETHProxy; UUPSProxy public regulationsManagerProxy; UUPSProxy public weETHProxy; @@ -140,8 +141,11 @@ contract TestSetup is Test { Liquifier public liquifierImplementation; Liquifier public liquifierInstance; - EtherFiRestaker public etherFiRestakerImplementation; - EtherFiRestaker public etherFiRestakerInstance; + EtherFiRestakeManager public etherFiRestakeManagerImplementation; + EtherFiRestakeManager public etherFiRestakeManagerInstance; + + RoleRegistry public roleRegistryImplementation; + RoleRegistry public roleRegistryInstance; EETH public eETHImplementation; EETH public eETHInstance; @@ -410,20 +414,28 @@ contract TestSetup is Test { // liquifierInstance.initializeRateLimiter(address(bucketRateLimiter)); - deployEtherFiRestaker(); + deployRoleRegistry(); + deployEtherFiRestakeManager(); vm.stopPrank(); } - function deployEtherFiRestaker() internal { - etherFiRestakerImplementation = new EtherFiRestaker(); - etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); - etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); + function deployEtherFiRestakeManager() internal { + etherFiRestakeManagerImplementation = new EtherFiRestakeManager(); + etherFiRestakeManagerProxy = new UUPSProxy(address(etherFiRestakeManagerImplementation), ""); + etherFiRestakeManagerInstance = EtherFiRestakeManager(payable(etherFiRestakeManagerProxy)); + + etherFiRestakeManagerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance), address(roleRegistryInstance)); + + liquifierInstance.initializeOnUpgrade(address(etherFiRestakeManagerInstance)); + } - etherFiRestakerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance), address(0x0)); - etherFiRestakerInstance.updateAdmin(alice, true); + function deployRoleRegistry() internal { + roleRegistryImplementation = new RoleRegistry(); + roleRegistryProxy = new UUPSProxy(address(roleRegistryImplementation), ""); + roleRegistryInstance = RoleRegistry(address(roleRegistryProxy)); - liquifierInstance.initializeOnUpgrade(address(etherFiRestakerInstance)); + roleRegistryInstance.initialize(owner); } function setUpTests() internal { @@ -568,9 +580,9 @@ contract TestSetup is Test { etherFiOracleProxy = new UUPSProxy(address(etherFiOracleImplementation), ""); etherFiOracleInstance = EtherFiOracle(payable(etherFiOracleProxy)); - etherFiRestakerImplementation = new EtherFiRestaker(); - etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); - etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); + etherFiRestakeManagerImplementation = new EtherFiRestakeManager(); + etherFiRestakeManagerProxy = new UUPSProxy(address(etherFiRestakeManagerImplementation), ""); + etherFiRestakeManagerInstance = EtherFiRestakeManager(payable(etherFiRestakeManagerProxy)); liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); membershipNftInstance.initialize("https://etherfi-cdn/{id}.json", address(membershipManagerInstance)); From 48e7816a82c53cc812e6f92505e90e9478913110 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 14 Nov 2024 19:55:04 -0600 Subject: [PATCH 3/6] updating tests and improving read APIs --- src/EtherFiRestakeManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EtherFiRestakeManager.sol b/src/EtherFiRestakeManager.sol index 1488d6a1f..7fd93b00c 100644 --- a/src/EtherFiRestakeManager.sol +++ b/src/EtherFiRestakeManager.sol @@ -35,12 +35,12 @@ contract EtherFiRestakeManager is Initializable, OwnableUpgradeable, UUPSUpgrade _disableInitializers(); } - function initialize(address _liquidityPool, address _liquifier, address _roleRegistry) initializer external { + function initialize(address _liquidityPool, address _liquifier, address _roleRegistry, address _etherFiRestakerImpl) initializer external { __Ownable_init(); __UUPSUpgradeable_init(); nextAvsOperatorId = 1; - upgradableBeacon = new UpgradeableBeacon(address(new EtherFiRestaker())); + upgradableBeacon = new UpgradeableBeacon(_etherFiRestakerImpl); liquidityPool = LiquidityPool(payable(_liquidityPool)); liquifier = Liquifier(payable(_liquifier)); roleRegistry = RoleRegistry(_roleRegistry); From cf738c123fec90a9abc8323dcb3ea1e093225257 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 14 Nov 2024 20:00:18 -0600 Subject: [PATCH 4/6] moving restaker impl creation --- test/TestSetup.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 620eee367..8b08eb0e2 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -425,7 +425,9 @@ contract TestSetup is Test { etherFiRestakeManagerProxy = new UUPSProxy(address(etherFiRestakeManagerImplementation), ""); etherFiRestakeManagerInstance = EtherFiRestakeManager(payable(etherFiRestakeManagerProxy)); - etherFiRestakeManagerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance), address(roleRegistryInstance)); + address etherFiRestakerImpl = address(new EtherFiRestaker()); + + etherFiRestakeManagerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance), address(roleRegistryInstance), address(etherFiRestakerImpl)); liquifierInstance.initializeOnUpgrade(address(etherFiRestakeManagerInstance)); } From 3c4168f71c5e0bb80e684ab80b2bb86a5a057e84 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 14 Nov 2024 20:24:29 -0600 Subject: [PATCH 5/6] beacon poxy test --- src/EtherFiRestakeManager.sol | 4 ++-- test/EtherFiRestaker.t.sol | 39 +++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/EtherFiRestakeManager.sol b/src/EtherFiRestakeManager.sol index 7fd93b00c..b3a536ccf 100644 --- a/src/EtherFiRestakeManager.sol +++ b/src/EtherFiRestakeManager.sol @@ -239,8 +239,8 @@ contract EtherFiRestakeManager is Initializable, OwnableUpgradeable, UUPSUpgrade return amount; } - /// @notice The assets controlled by the manager split between the 4 states - /// - restaked in Eigenlayer, + /// @notice The assets controlled by the manager split between 4 states + /// - restaked in Eigenlayer from an `EtherFiRestaker` instance /// - pending for un-restaking from Eigenlayer /// - non-restaked & held by this contract /// - non-restaked & pending in redemption for ETH diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol index 26695d8cd..db1aa4ba8 100644 --- a/test/EtherFiRestaker.t.sol +++ b/test/EtherFiRestaker.t.sol @@ -227,6 +227,20 @@ contract EtherFiRestakerTest is TestSetup { etherFiRestakeManagerInstance.undelegate(1); } + function test_change_operator() public { + test_delegate_to(); + + ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ + signature: hex"", + expiry: 0 + }); + + vm.startPrank(etherfiOperatingAdmin); + vm.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + etherFiRestakeManagerInstance.delegateTo(1, avsOperator2, signature, 0x0); + vm.stopPrank(); + } + function test_multi_restakers_multi_states() public { EtherFiRestaker etherFiRestakerInstance = etherFiRestakeManagerInstance.etherFiRestaker(1); @@ -272,17 +286,24 @@ contract EtherFiRestakerTest is TestSetup { } - function test_change_operator() public { - test_delegate_to(); + function test_beacon_upgrade() public { + _deposit_stEth(10 ether); - ISignatureUtils.SignatureWithExpiry memory signature = ISignatureUtils.SignatureWithExpiry({ - signature: hex"", - expiry: 0 - }); + vm.prank(owner); + etherFiRestakeManagerInstance.upgradeEtherFiRestaker(address(roleRegistryInstance)); - vm.startPrank(etherfiOperatingAdmin); - vm.expectRevert("DelegationManager._delegate: staker is already actively delegated"); - etherFiRestakeManagerInstance.delegateTo(1, avsOperator2, signature, 0x0); + vm.prank(alice); + vm.expectRevert(); + etherFiRestakeManagerInstance.depositIntoStrategy(1, address(stEth), 5 ether); + + vm.startPrank(owner); + etherFiRestakeManagerInstance.upgradeEtherFiRestaker(address(new EtherFiRestaker())); vm.stopPrank(); + + + vm.startPrank(alice); + etherFiRestakeManagerInstance.depositIntoStrategy(1, address(stEth), 5 ether); + etherFiRestakeManagerInstance.depositIntoStrategy(3, address(stEth), 5 ether); + } } From 71bb417a5daa92f8d3d8a532d569fa713aa62eb6 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 14 Nov 2024 20:40:26 -0600 Subject: [PATCH 6/6] comment clean up --- src/EtherFiRestakeManager.sol | 8 +++++--- src/EtherFiRestaker.sol | 3 ++- test/EtherFiRestaker.t.sol | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/EtherFiRestakeManager.sol b/src/EtherFiRestakeManager.sol index b3a536ccf..6748821f8 100644 --- a/src/EtherFiRestakeManager.sol +++ b/src/EtherFiRestakeManager.sol @@ -49,12 +49,13 @@ contract EtherFiRestakeManager is Initializable, OwnableUpgradeable, UUPSUpgrade lidoWithdrawalQueue = liquifier.lidoWithdrawalQueue(); } - - function upgradeEtherFiRestaker(address _newImplementation) public onlyOwner { + function upgradeEtherFiRestaker(address _newImplementation) external onlyOwner { upgradableBeacon.upgradeTo(_newImplementation); } - function instantiateEtherFiRestaker(uint256 _nums) external onlyOwner returns (uint256[] memory _ids) { + function instantiateEtherFiRestaker(uint256 _nums) external returns (uint256[] memory _ids) { + if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); + _ids = new uint256[](_nums); for (uint256 i = 0; i < _nums; i++) { _ids[i] = _instantiateEtherFiRestaker(); @@ -87,6 +88,7 @@ contract EtherFiRestakeManager is Initializable, OwnableUpgradeable, UUPSUpgrade } /// @notice undelegate from the current AVS operator & un-restake all + /// @dev Only considers stETH. Will need modification to support additional tokens /// @param index `EtherFiRestaker` instance to call `undelegate` on function undelegate(uint256 index) external returns (bytes32[] memory) { if (!roleRegistry.hasRole(RESTAKING_MANAGER_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); diff --git a/src/EtherFiRestaker.sol b/src/EtherFiRestaker.sol index dfa271115..8b105f6a5 100644 --- a/src/EtherFiRestaker.sol +++ b/src/EtherFiRestaker.sol @@ -68,6 +68,7 @@ contract EtherFiRestaker is Initializable { } /// @notice undelegate from the current AVS operator & un-restake all + /// @dev Only considers stETH. Will need modification to support additional tokens function undelegate() external managerOnly returns (bytes32[] memory) { // Un-restake all assets // Currently, only stETH is supported @@ -245,7 +246,7 @@ contract EtherFiRestaker is Initializable { TokenInfo memory info = tokenInfos[_token]; if (info.elStrategy != IStrategy(address(0))) { uint256 restakedTokenAmount = getRestakedAmount(_token); - restaked = liquifier.quoteByFairValue(_token, restakedTokenAmount); /// restaked & pending for withdrawals + restaked = liquifier.quoteByFairValue(_token, restakedTokenAmount); unrestaking = getEthAmountInEigenLayerPendingForWithdrawals(_token); } } diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol index db1aa4ba8..add525003 100644 --- a/test/EtherFiRestaker.t.sol +++ b/test/EtherFiRestaker.t.sol @@ -30,9 +30,10 @@ contract EtherFiRestakerTest is TestSetup { vm.startPrank(owner); liquifierInstance.updateQuoteStEthWithCurve(false); roleRegistryInstance.grantRole(etherFiRestakeManagerInstance.RESTAKING_MANAGER_ADMIN_ROLE(), etherfiOperatingAdmin); - etherFiRestakeManagerInstance.instantiateEtherFiRestaker(3); vm.stopPrank(); + vm.prank(etherfiOperatingAdmin); + etherFiRestakeManagerInstance.instantiateEtherFiRestaker(3); } function _deposit_stEth(uint256 _amount) internal {