From 82fab2eabebce87f7f53058e4d040b069d7dc0a2 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 9 Dec 2024 17:36:21 +0900 Subject: [PATCH] implemented instant fee mechanism, handling of the implicit fee --- src/EtherFiWithdrawalBuffer.sol | 89 +++++++++++------- src/WithdrawRequestNFT.sol | 79 +++++++++++++--- test/EtherFiWithdrawalBuffer.t.sol | 145 +++++++++++++++++++++++++++++ test/TestSetup.sol | 12 ++- test/WithdrawRequestNFT.t.sol | 18 ++-- 5 files changed, 288 insertions(+), 55 deletions(-) create mode 100644 test/EtherFiWithdrawalBuffer.t.sol diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index 95bf930c..811d8d14 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -30,24 +30,25 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU using SafeERC20 for IERC20; using Math for uint256; - uint256 private constant BUCKEt_UNIT_SCALE = 1e12; + uint256 private constant BUCKET_UNIT_SCALE = 1e12; uint256 private constant BASIS_POINT_SCALE = 1e4; - address public immutable feeReceiver; + address public immutable treasury; IeETH public immutable eEth; IWeETH public immutable weEth; ILiquidityPool public immutable liquidityPool; BucketLimiter.Limit public limit; - uint256 public exitFeeBasisPoints; - uint256 public lowWatermarkInBpsOfTvl; // bps of TVL + uint16 public exitFeeSplitToTreasuryInBps; + uint16 public exitFeeInBps; + uint16 public lowWatermarkInBpsOfTvl; // bps of TVL receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _liquidityPool, address _eEth, address _weEth, address _feeReceiver) { - require(address(liquidityPool) == address(0) && address(eEth) == address(0) && address(feeReceiver) == address(0), "EtherFiWithdrawalBuffer: Cannot initialize twice"); + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury) { + require(address(liquidityPool) == address(0) && address(eEth) == address(0) && address(treasury) == address(0), "EtherFiWithdrawalBuffer: Cannot initialize twice"); - feeReceiver = _feeReceiver; + treasury = _treasury; liquidityPool = ILiquidityPool(payable(_liquidityPool)); eEth = IeETH(_eEth); weEth = IWeETH(_weEth); @@ -55,14 +56,15 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU _disableInitializers(); } - function initialize(uint256 _exitFeeBasisPoints, uint256 _lowWatermarkInBpsOfTvl) external initializer { + function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl) external initializer { __Ownable_init(); __UUPSUpgradeable_init(); __Pausable_init(); __ReentrancyGuard_init(); limit = BucketLimiter.create(0, 0); - exitFeeBasisPoints = _exitFeeBasisPoints; + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + exitFeeInBps = _exitFeeInBps; lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; } @@ -74,9 +76,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @return The amount of ETH sent to the receiver and the exit fee amount. */ function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { - uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(eEthAmount); require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - require(eEthShares <= eEth.shares(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); @@ -118,18 +119,32 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function _redeem(uint256 ethAmount, address receiver) internal returns (uint256, uint256) { _updateRateLimit(ethAmount); - uint256 ethFeeAmount = _feeOnTotal(ethAmount, exitFeeBasisPoints); - uint256 ethToReceiver = ethAmount - ethFeeAmount; - - liquidityPool.withdraw(msg.sender, ethAmount); + uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); + uint256 ethShareToReceiver = ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE); + uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShareToReceiver); + uint256 prevLpBalance = address(liquidityPool).balance; uint256 prevBalance = address(this).balance; - payable(feeReceiver).transfer(ethFeeAmount); - payable(receiver).transfer(ethToReceiver); - uint256 ethSent = address(this).balance - prevBalance; - require(ethSent == ethAmount, "EtherFiWithdrawalBuffer: Transfer failed"); + uint256 burnedShares = (eEthAmountToReceiver > 0) ? liquidityPool.withdraw(address(this), eEthAmountToReceiver) : 0; + uint256 ethReceived = address(this).balance - prevBalance; + + uint256 ethShareFee = ethShares - burnedShares; + uint256 eEthAmountFee = liquidityPool.amountForShare(ethShareFee); + uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); + uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + + // To Stakers by burning shares + eEth.burnShares(address(this), liquidityPool.sharesForAmount(feeShareToStakers)); + + // To Treasury by transferring eETH + IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + + // To Receiver by transferring ETH + payable(receiver).transfer(ethReceived); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Transfer failed"); - return (ethToReceiver, ethFeeAmount); + return (ethReceived, eEthAmountFee); } /** @@ -143,6 +158,9 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @dev Returns the total amount that can be redeemed. */ function totalRedeemableAmount() external view returns (uint256) { + if (address(liquidityPool).balance < lowWatermarkInETH()) { + return 0; + } uint64 consumableBucketUnits = BucketLimiter.consumable(limit); uint256 consumableAmount = _convertBucketUnitToAmount(consumableBucketUnits); return consumableAmount; @@ -183,10 +201,21 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU /** * @dev Sets the exit fee. - * @param _exitFeeBasisPoints The exit fee. + * @param _exitFeeInBps The exit fee. */ - function setExitFeeBasisPoints(uint256 _exitFeeBasisPoints) external onlyOwner { - exitFeeBasisPoints = _exitFeeBasisPoints; + function setExitFeeBasisPoints(uint16 _exitFeeInBps) external onlyOwner { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeInBps = _exitFeeInBps; + } + + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external onlyOwner { + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external onlyOwner { + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; } function _updateRateLimit(uint256 shares) internal { @@ -195,11 +224,11 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU } function _convertSharesToBucketUnit(uint256 shares, Math.Rounding rounding) internal pure returns (uint64) { - return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((shares + BUCKEt_UNIT_SCALE - 1) / BUCKEt_UNIT_SCALE) : SafeCast.toUint64(shares / BUCKEt_UNIT_SCALE); + return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((shares + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(shares / BUCKET_UNIT_SCALE); } function _convertBucketUnitToAmount(uint64 bucketUnit) internal pure returns (uint256) { - return bucketUnit * BUCKEt_UNIT_SCALE; + return bucketUnit * BUCKET_UNIT_SCALE; } /** @@ -208,15 +237,11 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU // redeemable amount after exit fee function previewRedeem(uint256 shares) public view returns (uint256) { uint256 amountInEth = liquidityPool.amountForShare(shares); - return amountInEth - _feeOnTotal(amountInEth, exitFeeBasisPoints); + return amountInEth - _fee(amountInEth, exitFeeInBps); } - /** - * @dev Calculates the fee part of an amount `assets` that already includes fees. - * Used in {IERC4626-redeem}. - */ - function _feeOnTotal(uint256 assets, uint256 feeBasisPoints) internal pure virtual returns (uint256) { - return assets.mulDiv(feeBasisPoints, feeBasisPoints + BASIS_POINT_SCALE, Math.Rounding.Up); + function _fee(uint256 assets, uint256 feeBasisPoints) internal pure virtual returns (uint256) { + return assets.mulDiv(feeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Up); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 64d30ae4..439f0fa2 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -10,9 +10,15 @@ import "./interfaces/ILiquidityPool.sol"; import "./interfaces/IWithdrawRequestNFT.sol"; import "./interfaces/IMembershipManager.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgradeable, IWithdrawRequestNFT { + using Math for uint256; + uint256 private constant BASIS_POINT_SCALE = 1e4; + address public immutable treasury; + ILiquidityPool public liquidityPool; IeETH public eETH; IMembershipManager public membershipManager; @@ -22,16 +28,20 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 public nextRequestId; uint32 public lastFinalizedRequestId; - uint96 public accumulatedDustEEthShares; // to be burned or used to cover the validator churn cost + uint16 public shareRemainderSplitToTreasuryInBps; event WithdrawRequestCreated(uint32 indexed requestId, uint256 amountOfEEth, uint256 shareOfEEth, address owner, uint256 fee); event WithdrawRequestClaimed(uint32 indexed requestId, uint256 amountOfEEth, uint256 burntShareOfEEth, address owner, uint256 fee); event WithdrawRequestInvalidated(uint32 indexed requestId); event WithdrawRequestValidated(uint32 indexed requestId); event WithdrawRequestSeized(uint32 indexed requestId); + event HandledRemainderOfClaimedWithdrawRequests(uint256 eEthAmountToTreasury, uint256 eEthAmountBurnt); /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + constructor(address _treasury, uint16 _shareRemainderSplitToTreasuryInBps) { + treasury = _treasury; + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; + _disableInitializers(); } @@ -100,16 +110,13 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad _burn(tokenId); delete _requests[tokenId]; + uint256 amountBurnedShare = 0; if (fee > 0) { - // send fee to membership manager - liquidityPool.withdraw(address(membershipManager), fee); + amountBurnedShare += liquidityPool.withdraw(address(membershipManager), fee); } - - uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); + amountBurnedShare += liquidityPool.withdraw(recipient, amountToWithdraw); uint256 amountUnBurnedShare = request.shareOfEEth - amountBurnedShare; - if (amountUnBurnedShare > 0) { - accumulatedDustEEthShares += uint96(amountUnBurnedShare); - } + handleRemainder(amountUnBurnedShare); emit WithdrawRequestClaimed(uint32(tokenId), amountToWithdraw + fee, amountBurnedShare, recipient, fee); } @@ -120,13 +127,33 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } } - // a function to transfer accumulated shares to admin - function burnAccumulatedDustEEthShares() external onlyAdmin { - require(eETH.totalShares() > accumulatedDustEEthShares, "Inappropriate burn"); - uint256 amount = accumulatedDustEEthShares; - accumulatedDustEEthShares = 0; + // There have been errors tracking `accumulatedDustEEthShares` in the past. + // - https://github.com/etherfi-protocol/smart-contracts/issues/24 + // This is a one-time function to handle the remainder of the eEth shares after the claim of the withdraw requests + // It must be called only once with ALL the requests that have not been claimed yet. + // there are <3000 such requests and the total gas spending is expected to be ~9.0 M gas. + function handleAccumulatedShareRemainder(uint256[] memory _reqIds) external onlyOwner { + bytes32 slot = keccak256("handleAccumulatedShareRemainder"); + uint256 executed; + assembly { + executed := sload(slot) + } + require(executed == 0, "ALREADY_EXECUTED"); + + uint256 eEthSharesUnclaimedYet = 0; + for (uint256 i = 0; i < _reqIds.length; i++) { + assert (_requests[_reqIds[i]].isValid); + eEthSharesUnclaimedYet += _requests[_reqIds[i]].shareOfEEth; + } + uint256 eEthSharesRemainder = eETH.shares(address(this)) - eEthSharesUnclaimedYet; + + handleRemainder(eEthSharesRemainder); - eETH.burnShares(address(this), amount); + assembly { + sstore(slot, 1) + executed := sload(slot) + } + assert (executed == 1); } // Given an invalidated withdrawal request NFT of ID `requestId`:, @@ -196,6 +223,28 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad admins[_address] = _isAdmin; } + function updateShareRemainderSplitToTreasuryInBps(uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; + } + + /// @dev Handles the remainder of the eEth shares after the claim of the withdraw request + /// the remainder eETH share for a request = request.shareOfEEth - request.amountOfEEth / (eETH amount to eETH shares rate) + /// - Splits the remainder into two parts: + /// - Treasury: treasury gets a split of the remainder + /// - Burn: the rest of the remainder is burned + /// @param _eEthShares: the remainder of the eEth shares + function handleRemainder(uint256 _eEthShares) internal { + uint256 eEthSharesToTreasury = _eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); + + uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); + eETH.transfer(treasury, eEthAmountToTreasury); + + uint256 eEthSharesToBurn = _eEthShares - eEthSharesToTreasury; + eETH.burnShares(address(this), eEthSharesToBurn); + + emit HandledRemainderOfClaimedWithdrawRequests(eEthAmountToTreasury, liquidityPool.amountForShare(eEthSharesToBurn)); + } + // invalid NFTs is non-transferable except for the case they are being burnt by the owner via `seizeInvalidRequest` function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { for (uint256 i = 0; i < batchSize; i++) { diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol new file mode 100644 index 00000000..ce7f9954 --- /dev/null +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/console2.sol"; +import "./TestSetup.sol"; + +contract EtherFiWithdrawalBufferTest is TestSetup { + + address user = vm.addr(999); + + function setUp() public { + setUpTests(); + + vm.startPrank(owner); + etherFiWithdrawalBufferInstance.setCapacity(10 ether); + etherFiWithdrawalBufferInstance.setRefillRatePerSecond(0.001 ether); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(1e4); + vm.stopPrank(); + + vm.warp(block.timestamp + 5 * 1000); // 0.001 ether * 5000 = 5 ether refilled + + + } + + function test_rate_limit() public { + assertEq(etherFiWithdrawalBufferInstance.canRedeem(1 ether), true); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(5 ether - 1), true); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(5 ether + 1), false); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(10 ether), false); + assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), 5 ether); + } + + function test_lowwatermark_guardrail() public { + vm.deal(user, 100 ether); + + assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 0 ether); + + vm.prank(user); + liquidityPoolInstance.deposit{value: 100 ether}(); + + vm.startPrank(etherFiWithdrawalBufferInstance.owner()); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(1_00); // 1% + assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 1 ether); + + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(50_00); // 50% + assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 50 ether); + + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_00); // 100% + assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 100 ether); + } + + function test_redeem_eEth() public { + vm.deal(user, 100 ether); + vm.startPrank(user); + + assertEq(etherFiWithdrawalBufferInstance.canRedeem(1 ether), true); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(10 ether), false); + + liquidityPoolInstance.deposit{value: 1 ether}(); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 0.5 ether); + vm.expectRevert("TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE"); + etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 2 ether); + vm.expectRevert("EtherFiWithdrawalBuffer: Insufficient balance"); + etherFiWithdrawalBufferInstance.redeemEEth(2 ether, user, user); + + liquidityPoolInstance.deposit{value: 10 ether}(); + + uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); + assertEq(eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalance + 0.01 ether); + assertEq(address(user).balance, userBalance + 0.99 ether); + assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), totalRedeemableAmount - 1 ether); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 10 ether); + vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + etherFiWithdrawalBufferInstance.redeemEEth(10 ether, user, user); + + vm.stopPrank(); + } + + function test_redeem_weEth() public { + vm.deal(user, 100 ether); + vm.startPrank(user); + + assertEq(etherFiWithdrawalBufferInstance.canRedeem(1 ether), true); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(10 ether), false); + + liquidityPoolInstance.deposit{value: 1 ether}(); + eETHInstance.approve(address(weEthInstance), 1 ether); + weEthInstance.wrap(1 ether); + + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 0.5 ether); + vm.expectRevert("ERC20: insufficient allowance"); + etherFiWithdrawalBufferInstance.redeemWeEth(1 ether, user, user); + + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 2 ether); + vm.expectRevert("EtherFiWithdrawalBuffer: Insufficient balance"); + etherFiWithdrawalBufferInstance.redeemWeEth(2 ether, user, user); + + liquidityPoolInstance.deposit{value: 10 ether}(); + + uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemWeEth(1 ether, user, user); + assertEq(eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalance + 0.01 ether); + assertEq(address(user).balance, userBalance + 0.99 ether); + assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), totalRedeemableAmount - 1 ether); + + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 10 ether); + vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + etherFiWithdrawalBufferInstance.redeemWeEth(10 ether, user, user); + + vm.stopPrank(); + } + + function test_redeem_weEth_with_varying_exchange_rate() public { + vm.deal(user, 100 ether); + + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(weEthInstance), 1 ether); + weEthInstance.wrap(1 ether); + vm.stopPrank(); + + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(1 ether); // 10 eETH earned 1 ETH + + vm.startPrank(user); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemWeEth(1 ether, user, user); + assertEq(eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalance + 0.011 ether); + assertEq(address(user).balance, userBalance + (1.1 ether - 0.011 ether)); + vm.stopPrank(); + } +} diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 0f13eb4b..423a3b61 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -49,6 +49,7 @@ import "../src/EtherFiAdmin.sol"; import "../src/EtherFiTimelock.sol"; import "../src/BucketRateLimiter.sol"; +import "../src/EtherFiWithdrawalBuffer.sol"; contract TestSetup is Test { @@ -102,6 +103,7 @@ contract TestSetup is Test { UUPSProxy public membershipNftProxy; UUPSProxy public nftExchangeProxy; UUPSProxy public withdrawRequestNFTProxy; + UUPSProxy public etherFiWithdrawalBufferProxy; UUPSProxy public etherFiOracleProxy; UUPSProxy public etherFiAdminProxy; @@ -161,6 +163,8 @@ contract TestSetup is Test { WithdrawRequestNFT public withdrawRequestNFTImplementation; WithdrawRequestNFT public withdrawRequestNFTInstance; + EtherFiWithdrawalBuffer public etherFiWithdrawalBufferInstance; + NFTExchange public nftExchangeImplementation; NFTExchange public nftExchangeInstance; @@ -551,7 +555,7 @@ contract TestSetup is Test { membershipNftProxy = new UUPSProxy(address(membershipNftImplementation), ""); membershipNftInstance = MembershipNFT(payable(membershipNftProxy)); - withdrawRequestNFTImplementation = new WithdrawRequestNFT(); + withdrawRequestNFTImplementation = new WithdrawRequestNFT(address(0), 0); withdrawRequestNFTProxy = new UUPSProxy(address(withdrawRequestNFTImplementation), ""); withdrawRequestNFTInstance = WithdrawRequestNFT(payable(withdrawRequestNFTProxy)); @@ -572,7 +576,13 @@ contract TestSetup is Test { etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); + etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance))), ""); + etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); + etherFiWithdrawalBufferInstance.initialize(0, 1_00, 10_00); + liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); + liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); + membershipNftInstance.initialize("https://etherfi-cdn/{id}.json", address(membershipManagerInstance)); withdrawRequestNFTInstance.initialize(payable(address(liquidityPoolInstance)), payable(address(eETHInstance)), payable(address(membershipManagerInstance))); membershipManagerInstance.initialize( diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 98accc85..92454542 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -8,6 +8,8 @@ import "./TestSetup.sol"; contract WithdrawRequestNFTTest is TestSetup { + uint256[] public reqIds =[ 20, 388, 478, 714, 726, 729, 735, 815, 861, 916, 941, 1014, 1067, 1154, 1194, 1253]; + function setUp() public { setUpTests(); } @@ -179,7 +181,6 @@ contract WithdrawRequestNFTTest is TestSetup { vm.prank(bob); uint256 requestId = liquidityPoolInstance.requestWithdraw(bob, 1 ether); - assertEq(withdrawRequestNFTInstance.accumulatedDustEEthShares(), 0, "Accumulated dust should be 0"); assertEq(eETHInstance.balanceOf(bob), 9 ether); assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 1 ether, "eETH balance should be 1 ether"); @@ -202,12 +203,6 @@ contract WithdrawRequestNFTTest is TestSetup { assertEq(bobsEndingBalance, bobsStartingBalance + 1 ether, "Bobs balance should be 1 ether higher"); assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 1 ether, "eETH balance should be 1 ether"); - assertEq(liquidityPoolInstance.amountForShare(withdrawRequestNFTInstance.accumulatedDustEEthShares()), 1 ether); - - vm.prank(alice); - withdrawRequestNFTInstance.burnAccumulatedDustEEthShares(); - assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); - assertEq(eETHInstance.balanceOf(bob), 18 ether + 1 ether); // 1 ether eETH in `withdrawRequestNFT` contract is re-distributed to the eETH holders } function test_ValidClaimWithdrawWithNegativeRebase() public { @@ -418,4 +413,13 @@ contract WithdrawRequestNFTTest is TestSetup { assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); assertEq(address(chad).balance, chadBalance + claimableAmount, "Chad should receive the claimable amount"); } + + function test_distributeImplicitFee() public { + initializeRealisticFork(MAINNET_FORK); + + vm.startPrank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner), 50_00))); + + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds); + } }