diff --git a/audits/NM-0217 - EtherFi Restaking Of stETH Holdings.md b/audits/NM-0217 - EtherFi Restaking Of stETH Holdings.md new file mode 100644 index 00000000..eef5afb3 --- /dev/null +++ b/audits/NM-0217 - EtherFi Restaking Of stETH Holdings.md @@ -0,0 +1,36 @@ +# [NM-0217] Restaking of stETH holdings + +**File(s)**: [EtherFiRestaker.sol](https://github.com/etherfi-protocol/smart-contracts/blob/41836a2523b735fab8aad8ebfe4b25ed81fbc367/src/EtherFiRestaker.sol#L19), [Liquifier.sol](https://github.com/etherfi-protocol/smart-contracts/blob/41836a2523b735fab8aad8ebfe4b25ed81fbc367/src/Liquifier.sol#L50), +[ILiquifier.sol](https://github.com/etherfi-protocol/smart-contracts/blob/41836a2523b735fab8aad8ebfe4b25ed81fbc367/src/interfaces/ILiquifier.sol) + +### Summary + +Restaking of stETH holdings. Ether.fi is holding ~250k stETH in deVamp contract. However, it is suffering from the low capital efficiency being not deployed to EigenLayer restaking. This PR addresses this issue. + +--- + +### Findings + +### [Best practice] Lack of access control in the `undelegate` function + +**File(s)**: [EtherFiRestaker.sol](https://github.com/etherfi-protocol/smart-contracts/blob/41836a2523b735fab8aad8ebfe4b25ed81fbc367/src/EtherFiRestaker.sol#L147) + +**Description**: The `EtherFiRestaker::undelegate` function lacks access controls. The call will still revert because of the `onlyOwner` modifier that's used on the `queueWithdrawals` function, but this is the only "main" function in the contract without access controls. + +**Recommendation(s)**: Consider adding access control to this function. + +**Update from client**: fixed + +--- + +### [Best practice] Typo in function name `getEthAmountInEigenLayerPnedingForWithdrawals` + +**File(s)**: [EtherFiRestaker.sol](https://github.com/etherfi-protocol/smart-contracts/blob/41836a2523b735fab8aad8ebfe4b25ed81fbc367/src/EtherFiRestaker.sol#L330) + +**Description**: The function name has a typo `getEthAmountInEigenLayerPnedingForWithdrawals`. Instead if Pending, it is written Pneding. + +**Recommendation(s)**: Rename the function accordingly. + +**Update from client**: fixed + +--- diff --git a/operations/20241028_upgrade_liquifier.json b/operations/20241028_upgrade_liquifier.json new file mode 100644 index 00000000..a4d4e59d --- /dev/null +++ b/operations/20241028_upgrade_liquifier.json @@ -0,0 +1,25 @@ +{ "version": "1.0", "chainId": "1", "meta": { "name": "Transactions Batch", "description": "", "txBuilderVersion": "1.16.5", "createdFromSafeAddress": "0xcdd57D11476c22d265722F68390b036f3DA48c21" }, "transactions": [ +{ + "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", + "value": "0", + "data": "0x01d5062a0000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000243659cfe6000000000000000000000000b27b1dc838898368e9a81f69c626aec7e71f02c100000000000000000000000000000000000000000000000000000000" +} +, +{ + "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", + "value": "0", + "data": "0x134008d30000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000243659cfe6000000000000000000000000b27b1dc838898368e9a81f69c626aec7e71f02c100000000000000000000000000000000000000000000000000000000" +} +, +{ + "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", + "value": "0", + "data": "0x01d5062a0000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f4800000000000000000000000000000000000000000000000000000000000000024e587a6170000000000000000000000001b7a4c3797236a1c37f8741c0be35c2c72736fff00000000000000000000000000000000000000000000000000000000" +} +, +{ + "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", + "value": "0", + "data": "0x134008d30000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024e587a6170000000000000000000000001b7a4c3797236a1c37f8741c0be35c2c72736fff00000000000000000000000000000000000000000000000000000000" +} +] } diff --git a/src/EtherFiRestaker.sol b/src/EtherFiRestaker.sol new file mode 100644 index 00000000..d4d9c5dc --- /dev/null +++ b/src/EtherFiRestaker.sol @@ -0,0 +1,411 @@ +/// 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"; + +import "./Liquifier.sol"; +import "./LiquidityPool.sol"; + +import "./eigenlayer-interfaces/IStrategyManager.sol"; +import "./eigenlayer-interfaces/IDelegationManager.sol"; + +contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.Bytes32Set; + + struct TokenInfo { + // EigenLayer + IStrategy elStrategy; + uint256 elSharesInPendingForWithdrawals; + } + + LiquidityPool public liquidityPool; + Liquifier public liquifier; + 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(); + } + + /// @notice initialize to set variables on deployment + 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(); + + eigenLayerStrategyManager = liquifier.eigenLayerStrategyManager(); + eigenLayerDelegationManager = liquifier.eigenLayerDelegationManager(); + + (,, IStrategy strategy,,,,,,,,) = liquifier.tokenInfos(address(lido)); + tokenInfos[address(lido)] = TokenInfo({ + elStrategy: strategy, + elSharesInPendingForWithdrawals: 0 + }); + } + + 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 { + eigenLayerDelegationManager.delegateTo(operator, approverSignatureAndExpiry, approverSalt); + } + + // undelegate from the current AVS operator & un-restake all + function undelegate() external onlyAdmin 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); + + bytes32[] memory withdrawalRoots = eigenLayerDelegationManager.undelegate(address(this)); + assert(withdrawalRoots.length == 0); + + return withdrawalRoots; + } + + // deposit the token in holding into the restaking strategy + function depositIntoStrategy(address token, uint256 amount) external onlyAdmin returns (uint256) { + IERC20(token).safeApprove(address(eigenLayerStrategyManager), amount); + + IStrategy strategy = tokenInfos[token].elStrategy; + uint256 shares = eigenLayerStrategyManager.depositIntoStrategy(strategy, IERC20(token), amount); + + return shares; + } + + /// 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 onlyAdmin 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) { + uint256 currentNonce = eigenLayerDelegationManager.cumulativeWithdrawalsQueued(address(this)); + + bytes32[] memory withdrawalRoots = eigenLayerDelegationManager.queueWithdrawals(queuedWithdrawalParams); + IDelegationManager.Withdrawal[] memory withdrawals = new IDelegationManager.Withdrawal[](queuedWithdrawalParams.length); + + for (uint256 i = 0; i < queuedWithdrawalParams.length; i++) { + withdrawals[i] = IDelegationManager.Withdrawal({ + staker: address(this), + delegatedTo: eigenLayerDelegationManager.delegatedTo(address(this)), + withdrawer: address(this), + nonce: currentNonce + i, + startBlock: uint32(block.number), + strategies: queuedWithdrawalParams[i].strategies, + shares: queuedWithdrawalParams[i].shares + }); + + require(eigenLayerDelegationManager.calculateWithdrawalRoot(withdrawals[i]) == withdrawalRoots[i], "INCORRECT_WITHDRAWAL_ROOT"); + require(eigenLayerDelegationManager.pendingWithdrawals(withdrawalRoots[i]), "WITHDRAWAL_NOT_PENDING"); + + for (uint256 j = 0; j < queuedWithdrawalParams[i].strategies.length; j++) { + address token = address(queuedWithdrawalParams[i].strategies[j].underlyingToken()); + tokenInfos[token].elSharesInPendingForWithdrawals += queuedWithdrawalParams[i].shares[j]; + } + + withdrawalRootToWithdrawal[withdrawalRoots[i]] = withdrawals[i]; + withdrawalRootsSet.add(withdrawalRoots[i]); + } + + return withdrawalRoots; + } + + /// @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 { + bytes32[] memory withdrawalRoots = pendingWithdrawalRoots(); + + // process the first `max_cnt` withdrawals + uint256 num_to_process = _min(max_cnt, withdrawalRoots.length); + + IDelegationManager.Withdrawal[] memory _queuedWithdrawals = new IDelegationManager.Withdrawal[](num_to_process); + IERC20[][] memory _tokens = new IERC20[][](num_to_process); + uint256[] memory _middlewareTimesIndexes = new uint256[](num_to_process); + + uint256 cnt = 0; + for (uint256 i = 0; i < num_to_process; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawalRootToWithdrawal[withdrawalRoots[i]]; + + uint256 withdrawalDelay = eigenLayerDelegationManager.getWithdrawalDelay(withdrawal.strategies); + + if (withdrawal.startBlock + withdrawalDelay <= block.number) { + IERC20[] memory tokens = new IERC20[](withdrawal.strategies.length); + for (uint256 j = 0; j < withdrawal.strategies.length; j++) { + tokens[j] = withdrawal.strategies[j].underlyingToken(); + + assert(tokenInfos[address(tokens[j])].elStrategy == withdrawal.strategies[j]); + + tokenInfos[address(tokens[j])].elSharesInPendingForWithdrawals -= withdrawal.shares[j]; + } + + _queuedWithdrawals[cnt] = withdrawal; + _tokens[cnt] = tokens; + _middlewareTimesIndexes[cnt] = 0; + cnt += 1; + } + } + + if (cnt == 0) return; + + assembly { + mstore(_queuedWithdrawals, cnt) + mstore(_tokens, cnt) + mstore(_middlewareTimesIndexes, cnt) + } + + completeQueuedWithdrawals(_queuedWithdrawals, _tokens, _middlewareTimesIndexes); + } + + /// Advanced version + /// @notice Used to complete the specified `queuedWithdrawals`. The function caller must match `queuedWithdrawals[...].withdrawer` + /// @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 completeQueuedWithdrawals(IDelegationManager.Withdrawal[] memory _queuedWithdrawals, IERC20[][] memory _tokens, uint256[] memory _middlewareTimesIndexes) public onlyAdmin { + uint256 num = _queuedWithdrawals.length; + bool[] memory receiveAsTokens = new bool[](num); + for (uint256 i = 0; i < num; i++) { + bytes32 withdrawalRoot = eigenLayerDelegationManager.calculateWithdrawalRoot(_queuedWithdrawals[i]); + emit CompletedQueuedWithdrawal(withdrawalRoot); + + /// so that the shares withdrawn from the specified strategies are sent to the caller + receiveAsTokens[i] = true; + withdrawalRootsSet.remove(withdrawalRoot); + } + + /// it will update the erc20 balances of this contract + eigenLayerDelegationManager.completeQueuedWithdrawals(_queuedWithdrawals, _tokens, _middlewareTimesIndexes, receiveAsTokens); + } + + /// 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 + 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)); + } + + function getTotalPooledEther(address _token) public view returns (uint256) { + (uint256 restaked, uint256 unrestaking, uint256 holding, uint256 pendingForWithdrawals) = getTotalPooledEtherSplits(_token); + return restaked + unrestaking + holding + pendingForWithdrawals; + } + + function getRestakedAmount(address _token) public view returns (uint256) { + TokenInfo memory info = tokenInfos[_token]; + uint256 shares = eigenLayerStrategyManager.stakerStrategyShares(address(this), info.elStrategy); + uint256 restaked = info.elStrategy.sharesToUnderlyingView(shares); + return restaked; + } + + function getEigenLayerRestakingStrategy(address _token) public view returns (IStrategy) { + 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; + uint256 amount = info.elStrategy.sharesToUnderlyingView(info.elSharesInPendingForWithdrawals); + 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 onlyAdmin { + pausers[_address] = _isPauser; + } + + // Pauses the contract + function pauseContract() external onlyPauser { + _pause(); + } + + // Unpauses the contract + function unPauseContract() external onlyAdmin { + _unpause(); + } + + // INTERNAL functions + function _queueWithdrawlsByShares(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); + strategies[0] = strategy; + uint256[] memory sharesArr = new uint256[](1); + sharesArr[0] = shares; + + params[0] = IDelegationManager.QueuedWithdrawalParams({ + strategies: strategies, + shares: sharesArr, + withdrawer: address(this) + }); + + return queueWithdrawals(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 */ + modifier onlyAdmin() { + _requireAdmin(); + _; + } + + modifier onlyPauser() { + _requirePauser(); + _; + } +} \ No newline at end of file diff --git a/src/Liquifier.sol b/src/Liquifier.sol index 191cfe70..f142b30d 100644 --- a/src/Liquifier.sol +++ b/src/Liquifier.sol @@ -86,6 +86,8 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab mapping(address => bool) public pausers; + address public etherfiRestaker; + event Liquified(address _user, uint256 _toEEthAmount, address _fromToken, bool _isRestaked); event RegisteredQueuedWithdrawal(bytes32 _withdrawalRoot, IStrategyManager.DeprecatedStruct_QueuedWithdrawal _queuedWithdrawal); event RegisteredQueuedWithdrawal_V2(bytes32 _withdrawalRoot, IDelegationManager.Withdrawal _queuedWithdrawal); @@ -133,28 +135,12 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab DEPRECATED_eigenLayerWithdrawalClaimGasCost = 150_000; } - receive() external payable {} - - /// the users mint eETH given the queued withdrawal for their LRT with withdrawer == address(this) - /// @param _queuedWithdrawal The QueuedWithdrawal to be used for the deposit. This is the proof that the user has the re-staked ETH and requested the withdrawals setting the Liquifier contract as the withdrawer. - /// @param _referral The referral address - /// @return mintedAmount the amount of eETH minted to the caller (= msg.sender) - function depositWithQueuedWithdrawal(IDelegationManager.Withdrawal calldata _queuedWithdrawal, address _referral) external whenNotPaused nonReentrant returns (uint256) { - bytes32 withdrawalRoot = verifyQueuedWithdrawal(msg.sender, _queuedWithdrawal); - - /// register it to prevent duplicate deposits with the same queued withdrawal - isRegisteredQueuedWithdrawals[withdrawalRoot] = true; - emit RegisteredQueuedWithdrawal_V2(withdrawalRoot, _queuedWithdrawal); - - /// queue the strategy share for withdrawal - uint256 amount = _enqueueForWithdrawal(_queuedWithdrawal.strategies, _queuedWithdrawal.shares); - - /// mint eETH to the user - uint256 eEthShare = liquidityPool.depositToRecipient(msg.sender, amount, _referral); - - return eEthShare; + function initializeOnUpgrade(address _etherfiRestaker) external onlyOwner { + etherfiRestaker = _etherfiRestaker; } + receive() external payable {} + /// Deposit Liquid Staking Token such as stETH and Mint eETH /// @param _token The address of the token to deposit /// @param _amount The amount of the token to deposit @@ -164,7 +150,11 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab function depositWithERC20(address _token, uint256 _amount, address _referral) public whenNotPaused nonReentrant returns (uint256) { require(isTokenWhitelisted(_token) && (!tokenInfos[_token].isL2Eth || msg.sender == l1SyncPool), "NOT_ALLOWED"); - IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + if (tokenInfos[_token].isL2Eth) { + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + } else { + IERC20(_token).safeTransferFrom(msg.sender, address(etherfiRestaker), _amount); + } // The L1SyncPool's `_anticipatedDeposit` should be the only place to mint the `token` and always send its entirety to the Liquifier contract if(tokenInfos[_token].isL2Eth) _L2SanityChecks(_token); @@ -185,66 +175,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab return depositWithERC20(_token, _amount, _referral); } - /// @notice Used to complete the specified `queuedWithdrawals`. The function caller must match `queuedWithdrawals[...].withdrawer` - /// @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 completeQueuedWithdrawals(IDelegationManager.Withdrawal[] calldata _queuedWithdrawals, IERC20[][] calldata _tokens, uint256[] calldata _middlewareTimesIndexes) external onlyAdmin { - uint256 num = _queuedWithdrawals.length; - bool[] memory receiveAsTokens = new bool[](num); - for (uint256 i = 0; i < num; i++) { - _completeWithdrawals(_queuedWithdrawals[i]); - - /// so that the shares withdrawn from the specified strategies are sent to the caller - receiveAsTokens[i] = true; - } - - /// it will update the erc20 balances of this contract - eigenLayerDelegationManager.completeQueuedWithdrawals(_queuedWithdrawals, _tokens, _middlewareTimesIndexes, receiveAsTokens); - } - - /// Initiate the process for redemption of stETH - function stEthRequestWithdrawal() external onlyAdmin returns (uint256[] memory) { - uint256 amount = lido.balanceOf(address(this)); - return stEthRequestWithdrawal(amount); - } - - 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(); - - tokenInfos[address(lido)].ethAmountPendingForWithdrawals += uint128(_amount); - - 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); - uint256 newBalance = address(this).balance; - - // to prevent the underflow error - uint128 dx = uint128(_min(newBalance - balance, tokenInfos[address(lido)].ethAmountPendingForWithdrawals)); - tokenInfos[address(lido)].ethAmountPendingForWithdrawals -= dx; - - emit CompletedStEthQueuedWithdrawals(_requestIds); - } - // Send the redeemed ETH back to the liquidity pool & Send the fee to Treasury function withdrawEther() external onlyAdmin { uint256 amountToLiquidityPool = address(this).balance; @@ -252,6 +182,10 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab if (!sent) revert EthTransferFailed(); } + function sendToEtherFiRestaker(address _token, uint256 _amount) external onlyAdmin { + IERC20(_token).safeTransfer(etherfiRestaker, _amount); + } + function updateWhitelistedToken(address _token, bool _isWhitelisted) external onlyOwner { tokenInfos[_token].isWhitelisted = _isWhitelisted; } @@ -318,65 +252,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab return msg.value; } - // uint256 _amount, uint24 _fee, uint256 _minOutputAmount, uint256 _maxWaitingTime - function pancakeSwapForEth(address _token, uint256 _amount, uint24 _fee, uint256 _minOutputAmount, uint256 _maxWaitingTime) external onlyAdmin { - if (_amount > IERC20(_token).balanceOf(address(this))) revert NotEnoughBalance(); - uint256 beforeBalance = address(this).balance; - - IERC20(_token).approve(address(pancakeRouter), _amount); - - IPancackeV3SwapRouter.ExactInputSingleParams memory input = IPancackeV3SwapRouter.ExactInputSingleParams({ - tokenIn: _token, - tokenOut: pancakeRouter.WETH9(), - fee: _fee, - recipient: address(pancakeRouter), - deadline: block.timestamp + _maxWaitingTime, - amountIn: _amount, - amountOutMinimum: _minOutputAmount, - sqrtPriceLimitX96: 0 - }); - uint256 amountOut = pancakeRouter.exactInputSingle(input); - - pancakeRouter.unwrapWETH9(amountOut, address(this)); - - uint256 currentBalance = address(this).balance; - if (currentBalance < _minOutputAmount + beforeBalance) revert WrongOutput(); - } - - function swapCbEthToEth(uint256 _amount, uint256 _minOutputAmount) external onlyAdmin returns (uint256) { - if (_amount > cbEth.balanceOf(address(this))) revert NotEnoughBalance(); - cbEth.approve(address(cbEth_Eth_Pool), _amount); - return cbEth_Eth_Pool.exchange_underlying(1, 0, _amount, _minOutputAmount); - } - - function swapWbEthToEth(uint256 _amount, uint256 _minOutputAmount) external onlyAdmin returns (uint256) { - if (_amount > wbEth.balanceOf(address(this))) revert NotEnoughBalance(); - wbEth.approve(address(wbEth_Eth_Pool), _amount); - return wbEth_Eth_Pool.exchange(1, 0, _amount, _minOutputAmount); - } - - function swapStEthToEth(uint256 _amount, uint256 _minOutputAmount) external onlyAdmin returns (uint256) { - if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance(); - lido.approve(address(stEth_Eth_Pool), _amount); - return stEth_Eth_Pool.exchange(1, 0, _amount, _minOutputAmount); - } - - // https://etherscan.io/tx/0x4dde6b6d232f706466b18422b004e2584fd6c0d3c0afe40adfdc79c79031fe01 - // This user deposited 625 wBETH and minted only 7.65 eETH due to the low liquidity in the curve pool that it used - // Rescue him! - // function CASE1() external onlyAdmin { - // if (flags["CASE1"]) revert(); - // flags["CASE1"] = true; - - // // address recipient = 0xc0948cE48e87a55704EfEf8E4b8f92CA34D2087E; - // // uint256 wbethAmount = 625601006520000000000; - // // uint256 eEthAmount = 7650414487237129340; - // // uint256 exchagneRate = 1032800000000000000; // "the price of wBETH to ETH should be 1.0328" - // // uint256 diff = (wbethAmount * exchagneRate / 1e18) - eEthAmount; - // // uint256 diff = 617.302086995462789012 ether; - // liquidityPool.depositToRecipient(0xc0948cE48e87a55704EfEf8E4b8f92CA34D2087E, 617.302086995462789012 ether, address(0)); - // } - /* VIEW FUNCTIONS */ // Given the `_amount` of `_token` token, returns the equivalent amount of ETH @@ -424,19 +299,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab return (10000 - tokenInfos[_token].discountInBasisPoints) * marketValue / 10000; } - function verifyQueuedWithdrawal(address _user, IDelegationManager.Withdrawal calldata _queuedWithdrawal) public view returns (bytes32) { - require(_queuedWithdrawal.staker == _user && _queuedWithdrawal.withdrawer == address(this), "wrong depositor/withdrawer"); - for (uint256 i = 0; i < _queuedWithdrawal.strategies.length; i++) { - address token = address(_queuedWithdrawal.strategies[i].underlyingToken()); - require(tokenInfos[token].isWhitelisted && tokenInfos[token].strategy == _queuedWithdrawal.strategies[i], "NotWhitelisted"); - } - bytes32 withdrawalRoot = eigenLayerDelegationManager.calculateWithdrawalRoot(_queuedWithdrawal); - require(eigenLayerDelegationManager.pendingWithdrawals(withdrawalRoot), "WrongQ"); - require(!isRegisteredQueuedWithdrawals[withdrawalRoot], "Deposited"); - - return withdrawalRoot; - } - function isTokenWhitelisted(address _token) public view returns (bool) { return tokenInfos[_token].isWhitelisted; } @@ -499,47 +361,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab } /* INTERNAL FUNCTIONS */ - function _enqueueForWithdrawal(IStrategy[] memory _strategies, uint256[] memory _shares) internal returns (uint256) { - uint256 numStrategies = _strategies.length; - uint256 amount = 0; - for (uint256 i = 0; i < numStrategies; i++) { - IStrategy strategy = _strategies[i]; - uint256 share = _shares[i]; - address token = address(strategy.underlyingToken()); - uint256 dx = quoteStrategyShareForDeposit(token, strategy, share); - - // discount - dx = (10000 - tokenInfos[token].discountInBasisPoints) * dx / 10000; - - // Disable it because the deposit through EL queued withdrawal will be deprecated by EigenLayer anyway - // But, we need to still support '_enqueueForWithdrawal' as the backward compatibility for the already queued ones - // require(!isDepositCapReached(token, dx), "CAPPED"); - - amount += dx; - tokenInfos[token].strategyShare += uint128(share); - - _afterDeposit(token, amount); - - emit Liquified(msg.sender, dx, token, true); - } - return amount; - } - - function _completeWithdrawals(IDelegationManager.Withdrawal memory _queuedWithdrawal) internal { - bytes32 withdrawalRoot = eigenLayerDelegationManager.calculateWithdrawalRoot(_queuedWithdrawal); - - uint256 numStrategies = _queuedWithdrawal.strategies.length; - for (uint256 i = 0; i < numStrategies; i++) { - address token = address(_queuedWithdrawal.strategies[i].underlyingToken()); - uint128 share = uint128(_queuedWithdrawal.shares[i]); - - if (tokenInfos[token].strategyShare < share) revert StrategyShareNotEnough(); - tokenInfos[token].strategyShare -= share; - } - - emit CompletedQueuedWithdrawal(withdrawalRoot); - } - function _afterDeposit(address _token, uint256 _amount) internal { TokenInfo storage info = tokenInfos[_token]; if (block.timestamp >= info.timeBoundCapClockStartTime + timeBoundCapRefreshInterval) { @@ -554,7 +375,7 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab if (IERC20(_token).totalSupply() != IERC20(_token).balanceOf(address(this))) revert(); } - function _min(uint256 _a, uint256 _b) internal pure returns (uint256) { + function _min(uint256 _a, uint256 _b) internal pure returns (uint256) { return (_a > _b) ? _b : _a; } diff --git a/src/interfaces/ILiquifier.sol b/src/interfaces/ILiquifier.sol index e65ef52b..7fc22f97 100644 --- a/src/interfaces/ILiquifier.sol +++ b/src/interfaces/ILiquifier.sol @@ -68,6 +68,21 @@ interface IEigenLayerStrategyTVLLimits is IStrategy { // mainnet: 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1 interface ILidoWithdrawalQueue { + struct WithdrawalRequestStatus { + /// @notice stETH token amount that was locked on withdrawal queue for this request + uint256 amountOfStETH; + /// @notice amount of stETH shares locked on withdrawal queue for this request + uint256 amountOfShares; + /// @notice address that can claim or transfer this request + address owner; + /// @notice timestamp of when the request was created, in seconds + uint256 timestamp; + /// @notice true, if request is finalized + bool isFinalized; + /// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed) + bool isClaimed; + } + function FINALIZE_ROLE() external view returns (bytes32); function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); @@ -81,6 +96,8 @@ interface ILidoWithdrawalQueue { function getRoleMember(bytes32 _role, uint256 _index) external view returns (address); function getLastRequestId() external view returns (uint256); function getLastCheckpointIndex() external view returns (uint256); + function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds); + function getWithdrawalStatus(uint256[] memory _requestIds) external view returns (WithdrawalRequestStatus[] memory statuses); } interface ILiquifier { @@ -108,4 +125,5 @@ interface ILiquifier { } function depositWithERC20(address _token, uint256 _amount, address _referral) external returns (uint256); + function quoteByFairValue(address _token, uint256 _amount) external view returns (uint256); } diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol new file mode 100644 index 00000000..1af44d8a --- /dev/null +++ b/test/EtherFiRestaker.t.sol @@ -0,0 +1,228 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "./TestSetup.sol"; +import "forge-std/Test.sol"; + +import "@openzeppelin-upgradeable/contracts/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; + +import "../src/eigenlayer-interfaces/IDelegationManager.sol"; +import "../src/eigenlayer-interfaces/IStrategyManager.sol"; +import "../src/eigenlayer-interfaces/ISignatureUtils.sol"; + + +contract EtherFiRestakerTest is TestSetup { + + address avsOperator; + address avsOperator2; + address etherfiOperatingAdmin; + + function setUp() public { + initializeRealisticFork(MAINNET_FORK); + + setUpLiquifier(MAINNET_FORK); + + avsOperator = 0x5ACCC90436492F24E6aF278569691e2c942A676d; // EigenYields + avsOperator2 = 0xfB487f216CA24162119C0C6Ae015d680D7569C2f; + etherfiOperatingAdmin = alice; // + + vm.prank(owner); + 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)); + + 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; + + uint256 amount = 10 ether; + + _deposit_stEth(amount); + + assertEq(etherFiRestakerInstance.getEthAmountPendingForRedemption(address(stEth)), 0); + + 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); + } + + function test_restake_stEth() public { + uint256 currentStEthRestakedAmount = etherFiRestakerInstance.getRestakedAmount(address(stEth)); + + _deposit_stEth(10 ether); + + 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]); + + 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(); + } +} \ No newline at end of file diff --git a/test/EtherFiTimelock.sol b/test/EtherFiTimelock.sol index 34f529fc..855d9f5b 100644 --- a/test/EtherFiTimelock.sol +++ b/test/EtherFiTimelock.sol @@ -335,8 +335,16 @@ contract TimelockTest is TestSetup { initializeRealisticFork(MAINNET_FORK); { address target = address(liquifierInstance); - bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x5769ff35545B0BBFA27cc97C9407C5ed9d395455); + bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0xB27b1dc838898368E9a81F69c626AEC7e71f02c1); _execute_timelock(target, data, true, true, true, true); + + } + + { + address target = address(liquifierInstance); + bytes memory data = abi.encodeWithSelector(Liquifier.initializeOnUpgrade.selector, 0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf); + _execute_timelock(target, data, true, true, true, true); + } } diff --git a/test/Liquifier.t.sol b/test/Liquifier.t.sol index 31ad5782..ba666f01 100644 --- a/test/Liquifier.t.sol +++ b/test/Liquifier.t.sol @@ -158,240 +158,6 @@ contract LiquifierTest is TestSetup { liquifierInstance.depositWithERC20WithPermit(address(stEth), 1 ether, address(0), permitInput2); } - function test_withdrawal_of_non_restaked_stEth() public { - _setUp(MAINNET_FORK); - - uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - uint256 lpBalance = address(liquidityPoolInstance).balance; - - assertEq(eETHInstance.balanceOf(alice), 0); - assertEq(liquifierInstance.getTotalPooledEther(), 0); - - vm.deal(alice, 100 ether); - vm.startPrank(alice); - stEth.submit{value: 10 ether}(address(0)); - - ILiquidityPool.PermitInput memory permitInput = createPermitInput(2, address(liquifierInstance), 10 ether, 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), 10 ether, address(0), permitInput2); - - // Aliice has 10 ether eETH - // Total eETH TVL is 10 ether - assertGe(eETHInstance.balanceOf(alice), 10 ether - 0.1 ether); - assertGe(liquifierInstance.getTotalPooledEther(), 10 ether - 0.1 ether); - assertGe(liquidityPoolInstance.getTotalPooledEther(), lpTvl + 10 ether - 0.1 ether); - - // The protocol admin initiates the redemption process for 3500 stETH - uint256[] memory reqIds = liquifierInstance.stEthRequestWithdrawal(); - vm.stopPrank(); - - assertGe(eETHInstance.balanceOf(alice), 10 ether - 0.1 ether); - assertGe(liquifierInstance.getTotalPooledEther(), 10 ether - 0.1 ether); - assertGe(liquidityPoolInstance.getTotalPooledEther(), lpTvl + 10 ether - 0.1 ether); - - bytes32 FINALIZE_ROLE = liquifierInstance.lidoWithdrawalQueue().FINALIZE_ROLE(); - address finalize_role = liquifierInstance.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) = liquifierInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate); - liquifierInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate); - vm.stopPrank(); - - assertGe(eETHInstance.balanceOf(alice), 10 ether - 0.1 ether); - assertGe(liquifierInstance.getTotalPooledEther(), 10 ether - 0.1 ether); - assertGe(liquidityPoolInstance.getTotalPooledEther(), lpTvl + 10 ether - 0.1 ether); - - // The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract - vm.startPrank(alice); - uint256 lastCheckPointIndex = liquifierInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); - uint256[] memory hints = liquifierInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); - liquifierInstance.stEthClaimWithdrawals(reqIds, hints); - - assertGe(eETHInstance.balanceOf(alice), 10 ether - 0.1 ether); - assertGe(liquifierInstance.getTotalPooledEther(), 10 ether - 0.1 ether); - assertGe(liquidityPoolInstance.getTotalPooledEther(), lpTvl + 10 ether - 0.1 ether); - assertGe(address(liquidityPoolInstance).balance, lpBalance); - - // The ether.fi admin withdraws the ETH from the liquifier contract to the liquidity pool contract - liquifierInstance.withdrawEther(); - vm.stopPrank(); - - // the cycle completes - assertGe(eETHInstance.balanceOf(alice), 10 ether - 0.1 ether); - assertEq(liquifierInstance.getTotalPooledEther() / 100, 0); - assertGe(liquidityPoolInstance.getTotalPooledEther(), lpTvl + 10 ether - 0.1 ether); - assertGe(address(liquidityPoolInstance).balance + liquifierInstance.getTotalPooledEther(), lpBalance + 10 ether - 0.1 ether); - - } - - function test_withdrawal_of_restaked_wBETH_succeeds() internal { - _setUp(MAINNET_FORK); - - _enable_deposit(address(wbEthStrategy)); - - vm.deal(alice, 100 ether); - vm.startPrank(alice); - wbEth.deposit{value: 20 ether}(address(0)); - wbEth.approve(address(eigenLayerStrategyManager), 20 ether); - - eigenLayerStrategyManager.depositIntoStrategy(wbEthStrategy, wbEthStrategy.underlyingToken(), 10 ether); - - IDelegationManager.Withdrawal memory queuedWithdrawal = _get_queued_withdrawal_of_restaked_LST_before_m2(wbEthStrategy); - - _complete_queued_withdrawal_V2(queuedWithdrawal, wbEthStrategy); - - uint256[] memory reqIds = liquifierInstance.stEthRequestWithdrawal(); - vm.stopPrank(); - - _finalizeLidoWithdrawals(reqIds); - } - - function test_erc20_queued_withdrawal_v2() public { - initializeRealisticFork(MAINNET_FORK); - setUpLiquifier(MAINNET_FORK); - - uint256 liquifierTVL = liquifierInstance.getTotalPooledEther(); - uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - - // While this unit test works, after EL m2 upgrade, - // this flow will be deprecated because setting 'wtihdrawer' != msg.sender won't be allowed within `queueWithdrawals` - address actor = address(liquifierInstance); - - vm.deal(actor, 100 ether); - vm.startPrank(actor); - stEth.submit{value: 1 ether}(address(0)); - - stEth.approve(address(eigenLayerStrategyManager), 1 ether); - eigenLayerStrategyManager.depositIntoStrategy(stEthStrategy, stEthStrategy.underlyingToken(), 1 ether); - - - uint256[] memory strategyIndexes = new uint256[](1); - IStrategy[] memory strategies = new IStrategy[](1); - uint256[] memory shares = new uint256[](1); - strategyIndexes[0] = 0; - strategies[0] = stEthStrategy; - shares[0] = stEthStrategy.shares(actor); - - // Queue withdrawal - IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams = new IDelegationManager.QueuedWithdrawalParams[](1); - queuedWithdrawalParams[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategies, - shares: shares, - withdrawer: actor - }); - - IDelegationManager.Withdrawal[] memory queuedWithdrawals = new IDelegationManager.Withdrawal[](1); - queuedWithdrawals[0] = IDelegationManager.Withdrawal({ - staker: actor, - delegatedTo: address(0), - withdrawer: actor, - nonce: uint96(eigenLayerDelegationManager.cumulativeWithdrawalsQueued(actor)), - startBlock: uint32(block.number), - strategies: strategies, - shares: shares - }); - - bytes32[] memory withdrawalRoots = eigenLayerDelegationManager.queueWithdrawals(queuedWithdrawalParams); - bytes32 withdrawalRoot = withdrawalRoots[0]; - - assertTrue(eigenLayerDelegationManager.pendingWithdrawals(withdrawalRoot)); - - liquifierInstance.depositWithQueuedWithdrawal(queuedWithdrawals[0], address(0)); - vm.stopPrank(); - - vm.roll(block.number + 7 days); - - IERC20[][] memory tokens = new IERC20[][](1); - tokens[0] = new IERC20[](1); - tokens[0][0] = stEthStrategy.underlyingToken(); - uint256[] memory middlewareTimesIndexes = new uint256[](1); - middlewareTimesIndexes[0] = 0; - bool[] memory receiveAsTokens = new bool[](1); - receiveAsTokens[0] = true; - - vm.startPrank(owner); - liquifierInstance.completeQueuedWithdrawals(queuedWithdrawals, tokens, middlewareTimesIndexes); - vm.stopPrank(); - - } - - function _get_queued_withdrawal_of_restaked_LST_before_m2(IStrategy strategy) internal returns (IDelegationManager.Withdrawal memory) { - uint256[] memory strategyIndexes = new uint256[](1); - IStrategy[] memory strategies = new IStrategy[](1); - uint256[] memory shares = new uint256[](1); - strategyIndexes[0] = 0; - strategies[0] = strategy; - shares[0] = strategy.shares(alice); - - uint32 startBlock = uint32(block.number); - uint96 nonce = uint96(eigenLayerStrategyManager.numWithdrawalsQueued(alice)); - - // Step 1 - Queued withdrawal - bytes32 withdrawalRoot = eigenLayerStrategyManager.queueWithdrawal(strategyIndexes, strategies, shares, address(liquifierInstance), true); - assertEq(eigenLayerStrategyManager.withdrawalRootPending(withdrawalRoot), true); - vm.stopPrank(); - - _perform_eigenlayer_upgrade(); - - uint256 newNonce = eigenLayerDelegationManager.cumulativeWithdrawalsQueued(alice); - - vm.startPrank(alice); - - // Step 2 - Mint eETH - IDelegationManager.Withdrawal memory queuedWithdrawal = IDelegationManager.Withdrawal({ - staker: alice, - delegatedTo: address(0), - withdrawer: address(liquifierInstance), - nonce: newNonce, - startBlock: startBlock, - strategies: strategies, - shares: shares - }); - - // Before migration - vm.expectRevert("WrongQ"); - liquifierInstance.depositWithQueuedWithdrawal(queuedWithdrawal, address(0)); - - IStrategyManager.DeprecatedStruct_QueuedWithdrawal[] memory withdrawalsToMigrate = new IStrategyManager.DeprecatedStruct_QueuedWithdrawal[](1); - withdrawalsToMigrate[0] = IStrategyManager.DeprecatedStruct_QueuedWithdrawal({ - strategies: strategies, - shares: shares, - staker: alice, - withdrawerAndNonce: IStrategyManager.DeprecatedStruct_WithdrawerAndNonce({ - withdrawer: address(liquifierInstance), - nonce: nonce - }), - withdrawalStartBlock: startBlock, - delegatedAddress: address(0) - }); - assertEq(eigenLayerStrategyManager.calculateWithdrawalRoot(withdrawalsToMigrate[0]), withdrawalRoot); - - eigenLayerDelegationManager.migrateQueuedWithdrawals(withdrawalsToMigrate); - - IDelegationManager.Withdrawal memory migratedWithdrawal = IDelegationManager.Withdrawal({ - staker: withdrawalsToMigrate[0].staker, - delegatedTo: withdrawalsToMigrate[0].delegatedAddress, - withdrawer: withdrawalsToMigrate[0].withdrawerAndNonce.withdrawer, - nonce: withdrawalsToMigrate[0].withdrawerAndNonce.nonce, - startBlock: withdrawalsToMigrate[0].withdrawalStartBlock, - strategies: withdrawalsToMigrate[0].strategies, - shares: withdrawalsToMigrate[0].shares - }); - - bytes32 newWithdrawalRoot = eigenLayerDelegationManager.calculateWithdrawalRoot(migratedWithdrawal); - assertEq(eigenLayerDelegationManager.pendingWithdrawals(newWithdrawalRoot), true); - - liquifierInstance.depositWithQueuedWithdrawal(queuedWithdrawal, address(0)); - - // multipme mints using the same queued withdrawal fails - vm.expectRevert("Deposited"); - liquifierInstance.depositWithQueuedWithdrawal(queuedWithdrawal, address(0)); - - return queuedWithdrawal; - } - function _enable_deposit(address _strategy) internal { IEigenLayerStrategyTVLLimits strategyTVLLimits = IEigenLayerStrategyTVLLimits(_strategy); @@ -403,95 +169,6 @@ contract LiquifierTest is TestSetup { vm.stopPrank(); } - function _complete_queued_withdrawal(IStrategyManager.DeprecatedStruct_QueuedWithdrawal memory queuedWithdrawal, IStrategy strategy) internal { - vm.roll(block.number + 7 days); - - IStrategyManager.DeprecatedStruct_QueuedWithdrawal[] memory queuedWithdrawals = new IStrategyManager.DeprecatedStruct_QueuedWithdrawal[](1); - queuedWithdrawals[0] = queuedWithdrawal; - IERC20[][] memory tokens = new IERC20[][](1); - tokens[0] = new IERC20[](1); - tokens[0][0] = strategy.underlyingToken(); - uint256[] memory middlewareTimesIndexes = new uint256[](1); - middlewareTimesIndexes[0] = 0; - // liquifierInstance.completeQueuedWithdrawals(queuedWithdrawals, tokens, middlewareTimesIndexes); - - vm.expectRevert(); - // liquifierInstance.completeQueuedWithdrawals(queuedWithdrawals, tokens, middlewareTimesIndexes); - } - - function _complete_queued_withdrawal_V2(IDelegationManager.Withdrawal memory queuedWithdrawal, IStrategy strategy) internal { - vm.roll(block.number + 7 days); - - IDelegationManager.Withdrawal[] memory queuedWithdrawals = new IDelegationManager.Withdrawal[](1); - queuedWithdrawals[0] = queuedWithdrawal; - IERC20[][] memory tokens = new IERC20[][](1); - tokens[0] = new IERC20[](1); - tokens[0][0] = strategy.underlyingToken(); - uint256[] memory middlewareTimesIndexes = new uint256[](1); - middlewareTimesIndexes[0] = 0; - liquifierInstance.completeQueuedWithdrawals(queuedWithdrawals, tokens, middlewareTimesIndexes); - - vm.expectRevert(); - liquifierInstance.completeQueuedWithdrawals(queuedWithdrawals, tokens, middlewareTimesIndexes); - } - - - function test_pancacke_wbETH_swap() internal { - initializeRealisticFork(MAINNET_FORK); - setUpLiquifier(MAINNET_FORK); - - uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - uint256 lpBalance = address(liquidityPoolInstance).balance; - - uint256 inputAmount = 50 ether; - - vm.startPrank(alice); - - vm.expectRevert("Too little received"); - liquifierInstance.pancakeSwapForEth(address(wbEth), inputAmount, 500, 2 * inputAmount, 3600); - - uint256 beforeTVL = liquidityPoolInstance.getTotalPooledEther(); - uint256 beforeBalance = address(liquifierInstance).balance; - - uint256 exchangeRate = IWBETH(address(wbEth)).exchangeRate(); - uint256 maxSlippageBp = 50; // 0.5% - uint256 minOutput = (exchangeRate * inputAmount * (10000 - maxSlippageBp)) / 10000 / 1e18; - liquifierInstance.pancakeSwapForEth(address(wbEth), inputAmount, 500, minOutput, 3600); - - assertGe(address(liquifierInstance).balance, beforeBalance + minOutput); - assertEq(liquidityPoolInstance.getTotalPooledEther(), beforeTVL); // does not change till Oracle updates - - vm.stopPrank(); - } - - function test_pancacke_cbETH_swap() internal { - initializeRealisticFork(MAINNET_FORK); - setUpLiquifier(MAINNET_FORK); - - uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther(); - uint256 lpBalance = address(liquidityPoolInstance).balance; - - uint256 inputAmount = 50 ether; - - vm.startPrank(alice); - - vm.expectRevert("Too little received"); - liquifierInstance.pancakeSwapForEth(address(cbEth), inputAmount, 500, 2 * inputAmount, 3600); - - uint256 beforeTVL = liquidityPoolInstance.getTotalPooledEther(); - uint256 beforeBalance = address(liquifierInstance).balance; - - uint256 exchangeRate = IWBETH(address(cbEth)).exchangeRate(); - uint256 maxSlippageBp = 50; // 0.5% - uint256 minOutput = (exchangeRate * inputAmount * (10000 - maxSlippageBp)) / 10000 / 1e18; - liquifierInstance.pancakeSwapForEth(address(cbEth), inputAmount, 500, minOutput, 3600); - - assertGe(address(liquifierInstance).balance, beforeBalance + minOutput); - assertEq(liquidityPoolInstance.getTotalPooledEther(), beforeTVL); // does not change till Oracle updates - - vm.stopPrank(); - } - function _setup_L1SyncPool() internal { initializeRealisticFork(MAINNET_FORK); setUpLiquifier(MAINNET_FORK); diff --git a/test/TestSetup.sol b/test/TestSetup.sol index fa144b71..137494e2 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -27,6 +27,7 @@ import "../src/Treasury.sol"; import "../src/EtherFiNode.sol"; import "../src/LiquidityPool.sol"; import "../src/Liquifier.sol"; +import "../src/EtherFiRestaker.sol"; import "../src/EETH.sol"; import "../src/WeETH.sol"; import "../src/MembershipManager.sol"; @@ -92,6 +93,7 @@ contract TestSetup is Test { UUPSProxy public BNFTProxy; UUPSProxy public liquidityPoolProxy; UUPSProxy public liquifierProxy; + UUPSProxy public etherFiRestakerProxy; UUPSProxy public eETHProxy; UUPSProxy public regulationsManagerProxy; UUPSProxy public weETHProxy; @@ -138,6 +140,9 @@ contract TestSetup is Test { Liquifier public liquifierImplementation; Liquifier public liquifierInstance; + EtherFiRestaker public etherFiRestakerImplementation; + EtherFiRestaker public etherFiRestakerInstance; + EETH public eETHImplementation; EETH public eETHInstance; @@ -388,10 +393,7 @@ contract TestSetup is Test { function setUpLiquifier(uint8 forkEnum) internal { vm.startPrank(owner); - if (forkEnum == MAINNET_FORK) { - liquifierInstance.upgradeTo(address(new Liquifier())); - liquifierInstance.updateAdmin(alice, true); - } else if (forkEnum == TESTNET_FORK) { + if (forkEnum == MAINNET_FORK || forkEnum == TESTNET_FORK) { liquifierInstance.upgradeTo(address(new Liquifier())); liquifierInstance.updateAdmin(alice, true); } @@ -408,9 +410,22 @@ contract TestSetup is Test { // liquifierInstance.initializeRateLimiter(address(bucketRateLimiter)); + deployEtherFiRestaker(); + vm.stopPrank(); } + function deployEtherFiRestaker() internal { + etherFiRestakerImplementation = new EtherFiRestaker(); + etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); + etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); + + etherFiRestakerInstance.initialize(address(liquidityPoolInstance), address(liquifierInstance)); + etherFiRestakerInstance.updateAdmin(alice, true); + + liquifierInstance.initializeOnUpgrade(address(etherFiRestakerInstance)); + } + function setUpTests() internal { vm.startPrank(owner); @@ -553,6 +568,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)); 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)); @@ -1360,10 +1378,10 @@ contract TestSetup is Test { vm.startPrank(alice); uint256 lastCheckPointIndex = liquifierInstance.lidoWithdrawalQueue().getLastCheckpointIndex(); uint256[] memory hints = liquifierInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex); - liquifierInstance.stEthClaimWithdrawals(reqIds, hints); + etherFiRestakerInstance.stEthClaimWithdrawals(reqIds, hints); // The ether.fi admin withdraws the ETH from the liquifier contract to the liquidity pool contract - liquifierInstance.withdrawEther(); + etherFiRestakerInstance.withdrawEther(); vm.stopPrank(); }