From d356cc9e2e999bca4a66461717a44c5698fa52e2 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 6 Dec 2024 13:03:10 +0900 Subject: [PATCH 01/95] init. Instant Withdrawal via Buffer --- lib/BucketLimiter.sol | 5 + src/EtherFiWithdrawalBuffer.sol | 224 ++++++++++++++++++++++++++++++++ src/LiquidityPool.sol | 11 +- 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/EtherFiWithdrawalBuffer.sol diff --git a/lib/BucketLimiter.sol b/lib/BucketLimiter.sol index eb410f78e..83f749156 100644 --- a/lib/BucketLimiter.sol +++ b/lib/BucketLimiter.sol @@ -71,6 +71,11 @@ library BucketLimiter { return limit.remaining >= amount; } + function consumable(Limit memory limit) external view returns (uint64) { + _refill(limit); + return limit.remaining; + } + /* * Consumes the given amount from the bucket, if there is sufficient capacity, and returns * whether the bucket had enough remaining capacity to consume the amount. diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol new file mode 100644 index 000000000..95bf930ce --- /dev/null +++ b/src/EtherFiWithdrawalBuffer.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./interfaces/ILiquidityPool.sol"; +import "./interfaces/IeETH.sol"; +import "./interfaces/IWeETH.sol"; + +import "lib/BucketLimiter.sol"; + + +/* + The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. + - It has the exit fee as a percentage of the total amount redeemed. + - It has a rate limiter to limit the total amount that can be redeemed in a given time period. +*/ +contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 private constant BUCKEt_UNIT_SCALE = 1e12; + uint256 private constant BASIS_POINT_SCALE = 1e4; + address public immutable feeReceiver; + 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 + + 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"); + + feeReceiver = _feeReceiver; + liquidityPool = ILiquidityPool(payable(_liquidityPool)); + eEth = IeETH(_eEth); + weEth = IWeETH(_weEth); + + _disableInitializers(); + } + + function initialize(uint256 _exitFeeBasisPoints, uint256 _lowWatermarkInBpsOfTvl) external initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + limit = BucketLimiter.create(0, 0); + exitFeeBasisPoints = _exitFeeBasisPoints; + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + /** + * @notice Redeems eETH for ETH. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the eETH. + * @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"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + return _redeem(transferredEEthAmount, receiver); + } + + /** + * @notice Redeems weETH for ETH. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the weETH. + * @return The amount of ETH sent to the receiver and the exit fee amount. + */ + function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { + uint256 eEthShares = weEthAmount; + uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); + require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); + weEth.unwrap(weEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + return _redeem(transferredEEthAmount, receiver); + } + + + /** + * @notice Redeems ETH. + * @param ethAmount The amount of ETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @return The amount of ETH sent to the receiver and the exit fee amount. + */ + 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 prevBalance = address(this).balance; + payable(feeReceiver).transfer(ethFeeAmount); + payable(receiver).transfer(ethToReceiver); + uint256 ethSent = address(this).balance - prevBalance; + require(ethSent == ethAmount, "EtherFiWithdrawalBuffer: Transfer failed"); + + return (ethToReceiver, ethFeeAmount); + } + + /** + * @dev if the contract has less than the low watermark, it will not allow any instant redemption. + */ + function lowWatermarkInETH() public view returns (uint256) { + return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + } + + /** + * @dev Returns the total amount that can be redeemed. + */ + function totalRedeemableAmount() external view returns (uint256) { + uint64 consumableBucketUnits = BucketLimiter.consumable(limit); + uint256 consumableAmount = _convertBucketUnitToAmount(consumableBucketUnits); + return consumableAmount; + } + + /** + * @dev Returns whether the given amount can be redeemed. + * @param amount The ETH or eETH amount to check. + */ + function canRedeem(uint256 amount) public view returns (bool) { + if (address(liquidityPool).balance < lowWatermarkInETH()) { + return false; + } + uint64 bucketUnit = _convertSharesToBucketUnit(amount, Math.Rounding.Up); + bool consumable = BucketLimiter.canConsume(limit, bucketUnit); + return consumable; + } + + /** + * @dev Sets the maximum size of the bucket that can be consumed in a given time period. + * @param capacity The capacity of the bucket. + */ + function setCapacity(uint256 capacity) external onlyOwner { + // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough + uint64 bucketUnit = _convertSharesToBucketUnit(capacity, Math.Rounding.Down); + BucketLimiter.setCapacity(limit, bucketUnit); + } + + /** + * @dev Sets the rate at which the bucket is refilled per second. + * @param refillRate The rate at which the bucket is refilled per second. + */ + function setRefillRatePerSecond(uint256 refillRate) external onlyOwner { + // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough + uint64 bucketUnit = _convertSharesToBucketUnit(refillRate, Math.Rounding.Down); + BucketLimiter.setRefillRate(limit, bucketUnit); + } + + /** + * @dev Sets the exit fee. + * @param _exitFeeBasisPoints The exit fee. + */ + function setExitFeeBasisPoints(uint256 _exitFeeBasisPoints) external onlyOwner { + exitFeeBasisPoints = _exitFeeBasisPoints; + } + + function _updateRateLimit(uint256 shares) internal { + uint64 bucketUnit = _convertSharesToBucketUnit(shares, Math.Rounding.Up); + require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); + } + + 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); + } + + function _convertBucketUnitToAmount(uint64 bucketUnit) internal pure returns (uint256) { + return bucketUnit * BUCKEt_UNIT_SCALE; + } + + /** + * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. + */ + // redeemable amount after exit fee + function previewRedeem(uint256 shares) public view returns (uint256) { + uint256 amountInEth = liquidityPool.amountForShare(shares); + return amountInEth - _feeOnTotal(amountInEth, exitFeeBasisPoints); + } + + /** + * @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 _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + +} \ No newline at end of file diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 93e032abf..28ee11fdd 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -23,6 +23,9 @@ import "./interfaces/IEtherFiAdmin.sol"; import "./interfaces/IAuctionManager.sol"; import "./interfaces/ILiquifier.sol"; +import "./EtherFiWithdrawalBuffer.sol"; + + contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, ILiquidityPool { //-------------------------------------------------------------------------------------- //--------------------------------- STATE-VARIABLES ---------------------------------- @@ -69,6 +72,8 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL bool private isLpBnftHolder; + EtherFiWithdrawalBuffer public etherFiWithdrawalBuffer; + //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- //-------------------------------------------------------------------------------------- @@ -139,6 +144,10 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL liquifier = ILiquifier(_liquifier); } + function initializeOnUpgradeWithWithdrawalBuffer(address _withdrawalBuffer) external onlyOwner { + etherFiWithdrawalBuffer = EtherFiWithdrawalBuffer(payable(_withdrawalBuffer)); + } + // Used by eETH staking flow function deposit() external payable returns (uint256) { return deposit(address(0)); @@ -179,7 +188,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL /// it returns the amount of shares burned function withdraw(address _recipient, uint256 _amount) external whenNotPaused returns (uint256) { uint256 share = sharesForWithdrawalAmount(_amount); - require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager), "Incorrect Caller"); + require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiWithdrawalBuffer), "Incorrect Caller"); if (totalValueInLp < _amount || (msg.sender == address(withdrawRequestNFT) && ethAmountLockedForWithdrawal < _amount) || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); if (_amount > type(uint128).max || _amount == 0 || share == 0) revert InvalidAmount(); From 82fab2eabebce87f7f53058e4d040b069d7dc0a2 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 9 Dec 2024 17:36:21 +0900 Subject: [PATCH 02/95] 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 95bf930ce..811d8d149 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 64d30ae4f..439f0fa24 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 000000000..ce7f99548 --- /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 0f13eb4b9..423a3b612 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 98accc85b..924545421 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); + } } From 0f13953a94b65a17b61c4491544a80274c14af03 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 12 Dec 2024 09:33:34 +0900 Subject: [PATCH 03/95] fix scripts --- script/deploys/DeployPhaseTwo.s.sol | 2 +- script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/deploys/DeployPhaseTwo.s.sol b/script/deploys/DeployPhaseTwo.s.sol index a144e0a35..2417e191b 100644 --- a/script/deploys/DeployPhaseTwo.s.sol +++ b/script/deploys/DeployPhaseTwo.s.sol @@ -81,7 +81,7 @@ contract DeployPhaseTwoScript is Script { } retrieve_contract_addresses(); - withdrawRequestNftImplementation = new WithdrawRequestNFT(); + withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0), 0); withdrawRequestNftProxy = new UUPSProxy(address(withdrawRequestNftImplementation), ""); withdrawRequestNftInstance = WithdrawRequestNFT(payable(withdrawRequestNftProxy)); diff --git a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol index 823cf90a5..5862fb36e 100644 --- a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol +++ b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol @@ -20,7 +20,7 @@ contract WithdrawRequestNFTUpgrade is Script { vm.startBroadcast(deployerPrivateKey); WithdrawRequestNFT oracleInstance = WithdrawRequestNFT(proxyAddress); - WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(); + WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(address(0), 0); oracleInstance.upgradeTo(address(v2Implementation)); From 0b68310623d61f3c8ebc481e5b3f90743e237f52 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 17 Dec 2024 14:43:25 +0900 Subject: [PATCH 04/95] add role registry, consider eth amount locked for withdrawal in liquidity pool, add tests --- src/EtherFiWithdrawalBuffer.sol | 85 ++++++--- src/interfaces/ILiquidityPool.sol | 1 + test/EtherFiWithdrawalBuffer.t.sol | 271 ++++++++++++++++++++++++++++- test/TestSetup.sol | 14 +- 4 files changed, 336 insertions(+), 35 deletions(-) diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index 811d8d149..a79b9e948 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -20,6 +20,7 @@ import "./interfaces/IWeETH.sol"; import "lib/BucketLimiter.sol"; +import "./RoleRegistry.sol"; /* The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. @@ -32,6 +33,12 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 private constant BUCKET_UNIT_SCALE = 1e12; uint256 private constant BASIS_POINT_SCALE = 1e4; + + bytes32 public constant PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); + bytes32 public constant PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); + bytes32 public constant PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); + + RoleRegistry public immutable roleRegistry; address public immutable treasury; IeETH public immutable eEth; IWeETH public immutable weEth; @@ -45,9 +52,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor - 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"); - + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { + roleRegistry = RoleRegistry(_roleRegistry); treasury = _treasury; liquidityPool = ILiquidityPool(payable(_liquidityPool)); eEth = IeETH(_eEth); @@ -56,13 +62,13 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU _disableInitializers(); } - function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl) external initializer { + function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { __Ownable_init(); __UUPSUpgradeable_init(); __Pausable_init(); __ReentrancyGuard_init(); - limit = BucketLimiter.create(0, 0); + limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; exitFeeInBps = _exitFeeInBps; lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; @@ -76,8 +82,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) { - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); @@ -97,8 +103,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { uint256 eEthShares = weEthAmount; uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); @@ -135,14 +141,14 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; // To Stakers by burning shares - eEth.burnShares(address(this), liquidityPool.sharesForAmount(feeShareToStakers)); + eEth.burnShares(address(this), 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"); + (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); + require(success && address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Transfer failed"); return (ethReceived, eEthAmountFee); } @@ -158,12 +164,13 @@ 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()) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { return 0; } uint64 consumableBucketUnits = BucketLimiter.consumable(limit); - uint256 consumableAmount = _convertBucketUnitToAmount(consumableBucketUnits); - return consumableAmount; + uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); + return Math.min(consumableAmount, liquidEthAmount); } /** @@ -171,21 +178,22 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param amount The ETH or eETH amount to check. */ function canRedeem(uint256 amount) public view returns (bool) { - if (address(liquidityPool).balance < lowWatermarkInETH()) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { return false; } - uint64 bucketUnit = _convertSharesToBucketUnit(amount, Math.Rounding.Up); + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); bool consumable = BucketLimiter.canConsume(limit, bucketUnit); - return consumable; + return consumable && amount <= liquidEthAmount; } /** * @dev Sets the maximum size of the bucket that can be consumed in a given time period. * @param capacity The capacity of the bucket. */ - function setCapacity(uint256 capacity) external onlyOwner { + function setCapacity(uint256 capacity) external hasRole(PROTOCOL_ADMIN) { // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough - uint64 bucketUnit = _convertSharesToBucketUnit(capacity, Math.Rounding.Down); + uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); BucketLimiter.setCapacity(limit, bucketUnit); } @@ -193,9 +201,9 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @dev Sets the rate at which the bucket is refilled per second. * @param refillRate The rate at which the bucket is refilled per second. */ - function setRefillRatePerSecond(uint256 refillRate) external onlyOwner { + function setRefillRatePerSecond(uint256 refillRate) external hasRole(PROTOCOL_ADMIN) { // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough - uint64 bucketUnit = _convertSharesToBucketUnit(refillRate, Math.Rounding.Down); + uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); BucketLimiter.setRefillRate(limit, bucketUnit); } @@ -203,31 +211,39 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @dev Sets the exit fee. * @param _exitFeeInBps The exit fee. */ - function setExitFeeBasisPoints(uint16 _exitFeeInBps) external onlyOwner { + function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(PROTOCOL_ADMIN) { require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); exitFeeInBps = _exitFeeInBps; } - function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external onlyOwner { + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(PROTOCOL_ADMIN) { require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; } - function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external onlyOwner { + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(PROTOCOL_ADMIN) { require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; } - function _updateRateLimit(uint256 shares) internal { - uint64 bucketUnit = _convertSharesToBucketUnit(shares, Math.Rounding.Up); + function pauseContract() external hasRole(PROTOCOL_PAUSER) { + _pause(); + } + + function unPauseContract() external hasRole(PROTOCOL_UNPAUSER) { + _unpause(); + } + + function _updateRateLimit(uint256 amount) internal { + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); } - 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); + function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); } - function _convertBucketUnitToAmount(uint64 bucketUnit) internal pure returns (uint256) { + function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { return bucketUnit * BUCKET_UNIT_SCALE; } @@ -246,4 +262,17 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + function _hasRole(bytes32 role, address account) internal view returns (bool) { + require(roleRegistry.hasRole(role, account), "EtherFiWithdrawalBuffer: Unauthorized"); + } + + modifier hasRole(bytes32 role) { + _hasRole(role, msg.sender); + _; + } + } \ No newline at end of file diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 2f12fd76d..a71b2d6c7 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -50,6 +50,7 @@ interface ILiquidityPool { function sharesForAmount(uint256 _amount) external view returns (uint256); function sharesForWithdrawalAmount(uint256 _amount) external view returns (uint256); function amountForShare(uint256 _share) external view returns (uint256); + function ethAmountLockedForWithdrawal() external view returns (uint128); function deposit() external payable returns (uint256); function deposit(address _referral) external payable returns (uint256); diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol index ce7f99548..d73268fd0 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -7,6 +7,7 @@ import "./TestSetup.sol"; contract EtherFiWithdrawalBufferTest is TestSetup { address user = vm.addr(999); + address op_admin = vm.addr(1000); function setUp() public { setUpTests(); @@ -18,8 +19,18 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.stopPrank(); vm.warp(block.timestamp + 5 * 1000); // 0.001 ether * 5000 = 5 ether refilled + } + + function setUp_Fork() public { + initializeRealisticFork(MAINNET_FORK); + vm.startPrank(owner); + roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), op_admin); + vm.stopPrank(); + etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); + etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); + etherFiWithdrawalBufferInstance.initialize(1e4, 1_00, 10_00); // 10% fee split to treasury, 1% exit fee, 10% low watermark } function test_rate_limit() public { @@ -77,14 +88,38 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertEq(address(user).balance, userBalance + 0.99 ether); assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), totalRedeemableAmount - 1 ether); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 10 ether); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(10 ether, user, user); + etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); + + vm.stopPrank(); + } + + function test_mainnet_redeem_weEth_with_rebase() public { + vm.deal(user, 100 ether); + + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(weEthInstance), 10 ether); + weEthInstance.wrap(10 ether); + vm.stopPrank(); + + uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 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.0101 ether); + assertEq(address(user).balance, userBalance + 0.9999 ether); vm.stopPrank(); } - function test_redeem_weEth() public { + function test_redeem_weEth_1() public { vm.deal(user, 100 ether); vm.startPrank(user); @@ -114,9 +149,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertEq(address(user).balance, userBalance + 0.99 ether); assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), totalRedeemableAmount - 1 ether); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 10 ether); + eETHInstance.approve(address(weEthInstance), 6 ether); + weEthInstance.wrap(6 ether); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemWeEth(10 ether, user, user); + etherFiWithdrawalBufferInstance.redeemWeEth(5 ether, user, user); vm.stopPrank(); } @@ -142,4 +179,228 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertEq(address(user).balance, userBalance + (1.1 ether - 0.011 ether)); vm.stopPrank(); } + + // The test ensures that: + // - Redemption works correctly within allowed limits. + // - Fees are applied accurately. + // - The function properly reverts when redemption conditions aren't met. + function testFuzz_redeemEEth( + uint256 depositAmount, + uint256 redeemAmount, + uint256 exitFeeSplitBps, + uint16 exitFeeBps, + uint16 lowWatermarkBps + ) public { + depositAmount = bound(depositAmount, 1 ether, 1000 ether); + redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); + exitFeeSplitBps = bound(exitFeeSplitBps, 0, 10000); + exitFeeBps = uint16(bound(uint256(exitFeeBps), 0, 10000)); + lowWatermarkBps = uint16(bound(uint256(lowWatermarkBps), 0, 10000)); + + vm.deal(user, depositAmount); + vm.startPrank(user); + liquidityPoolInstance.deposit{value: depositAmount}(); + vm.stopPrank(); + + // Set exitFeeSplitToTreasuryInBps + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + + // Set exitFeeBasisPoints and lowWatermarkInBpsOfTvl + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + + vm.prank(owner); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + + vm.startPrank(user); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); + uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + + if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + uint256 userBalanceBefore = address(user).balance; + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); + + etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + + uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; + uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; + uint256 userReceives = redeemAmount - totalFee; + + assertApproxEqAbs( + eETHInstance.balanceOf(address(treasuryInstance)), + treasuryBalanceBefore + treasuryFee, + 1e1 + ); + assertApproxEqAbs( + address(user).balance, + userBalanceBefore + userReceives, + 1e1 + ); + + } else { + vm.expectRevert(); + etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + } + vm.stopPrank(); + } + + function testFuzz_redeemWeEth( + uint256 depositAmount, + uint256 redeemAmount, + uint256 exitFeeSplitBps, + uint16 exitFeeBps, + uint16 lowWatermarkBps + ) public { + // Bound the parameters + depositAmount = bound(depositAmount, 1 ether, 1000 ether); + redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); + exitFeeSplitBps = bound(exitFeeSplitBps, 0, 10000); + exitFeeBps = uint16(bound(uint256(exitFeeBps), 0, 10000)); + lowWatermarkBps = uint16(bound(uint256(lowWatermarkBps), 0, 10000)); + + // Deal Ether to user and perform deposit + vm.deal(user, depositAmount); + vm.startPrank(user); + liquidityPoolInstance.deposit{value: depositAmount}(); + vm.stopPrank(); + + // Set fee and watermark configurations + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + + vm.prank(owner); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + + // User approves weETH and attempts redemption + vm.startPrank(user); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); + uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + + if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + uint256 userBalanceBefore = address(user).balance; + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); + + etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + + uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; + uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; + uint256 userReceives = redeemAmount - totalFee; + + assertApproxEqAbs( + eETHInstance.balanceOf(address(treasuryInstance)), + treasuryBalanceBefore + treasuryFee, + 1e1 + ); + assertApproxEqAbs( + address(user).balance, + userBalanceBefore + userReceives, + 1e1 + ); + + } else { + vm.expectRevert(); + etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + } + vm.stopPrank(); + } + + function testFuzz_role_management(address admin, address pauser, address unpauser, address user) public { + address owner = roleRegistry.owner(); + bytes32 PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); + bytes32 PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); + bytes32 PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); + + vm.assume(admin != address(0) && admin != owner); + vm.assume(pauser != address(0) && pauser != owner && pauser != admin); + vm.assume(unpauser != address(0) && unpauser != owner && unpauser != admin && unpauser != pauser); + vm.assume(user != address(0) && user != owner && user != admin && user != pauser && user != unpauser); + + // Grant roles to respective addresses + vm.prank(owner); + roleRegistry.grantRole(PROTOCOL_ADMIN, admin); + vm.prank(owner); + roleRegistry.grantRole(PROTOCOL_PAUSER, pauser); + vm.prank(owner); + roleRegistry.grantRole(PROTOCOL_UNPAUSER, unpauser); + + // Admin performs admin-only actions + vm.startPrank(admin); + etherFiWithdrawalBufferInstance.setCapacity(10 ether); + etherFiWithdrawalBufferInstance.setRefillRatePerSecond(0.001 ether); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(1e4); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(1e2); + etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(1e2); + vm.stopPrank(); + + // Pauser pauses the contract + vm.startPrank(pauser); + etherFiWithdrawalBufferInstance.pauseContract(); + assertTrue(etherFiWithdrawalBufferInstance.paused()); + vm.stopPrank(); + + // Unpauser unpauses the contract + vm.startPrank(unpauser); + etherFiWithdrawalBufferInstance.unPauseContract(); + assertFalse(etherFiWithdrawalBufferInstance.paused()); + vm.stopPrank(); + + // Revoke PROTOCOL_ADMIN role from admin + vm.prank(owner); + roleRegistry.revokeRole(PROTOCOL_ADMIN, admin); + + // Admin attempts admin-only actions after role revocation + vm.startPrank(admin); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.setCapacity(10 ether); + vm.stopPrank(); + + // Pauser attempts to unpause (should fail) + vm.startPrank(pauser); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.unPauseContract(); + vm.stopPrank(); + + // Unpauser attempts to pause (should fail) + vm.startPrank(unpauser); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.pauseContract(); + vm.stopPrank(); + + // User without role attempts admin-only actions + vm.startPrank(user); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.pauseContract(); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.unPauseContract(); + vm.stopPrank(); + } + + function test_mainnet_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}(); + + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); + + assertEq(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + 0.01 ether); + assertEq(address(user).balance, userBalance + 0.99 ether); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); + vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); + + vm.stopPrank(); + } } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 423a3b612..9c9fe030c 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -106,6 +106,7 @@ contract TestSetup is Test { UUPSProxy public etherFiWithdrawalBufferProxy; UUPSProxy public etherFiOracleProxy; UUPSProxy public etherFiAdminProxy; + UUPSProxy public roleRegistryProxy; DepositDataGeneration public depGen; IDepositContract public depositContractEth2; @@ -190,6 +191,8 @@ contract TestSetup is Test { EtherFiTimelock public etherFiTimelockInstance; BucketRateLimiter public bucketRateLimiter; + RoleRegistry public roleRegistry; + bytes32 root; bytes32 rootMigration; bytes32 rootMigration2; @@ -392,6 +395,7 @@ contract TestSetup is Test { etherFiTimelockInstance = EtherFiTimelock(payable(addressProviderInstance.getContractAddress("EtherFiTimelock"))); etherFiAdminInstance = EtherFiAdmin(payable(addressProviderInstance.getContractAddress("EtherFiAdmin"))); etherFiOracleInstance = EtherFiOracle(payable(addressProviderInstance.getContractAddress("EtherFiOracle"))); + roleRegistry = RoleRegistry(0x1d3Af47C1607A2EF33033693A9989D1d1013BB50); } function setUpLiquifier(uint8 forkEnum) internal { @@ -576,9 +580,15 @@ 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))), ""); + roleRegistryProxy = new UUPSProxy(address(new RoleRegistry()), ""); + roleRegistry = RoleRegistry(address(roleRegistryProxy)); + roleRegistry.initialize(owner); + + etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(0, 1_00, 10_00); + etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + + roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), owner); liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); From c9fd604d8b0017e7b9faa5cdbf017ea374a827cc Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 19 Dec 2024 16:38:40 +0900 Subject: [PATCH 05/95] use setter for 'shareRemainderSplitToTreasuryInBps', add more fuzz tests, add deploy script --- .../DeployEtherFiWithdrawalBuffer.s.sol | 40 +++ script/deploys/DeployPhaseTwo.s.sol | 2 +- .../WithdrawRequestNFTUpgradeScript.s.sol | 2 +- src/WithdrawRequestNFT.sol | 3 +- test/EtherFiWithdrawalBuffer.t.sol | 270 +++++++----------- test/TestSetup.sol | 2 +- test/WithdrawRequestNFT.t.sol | 254 ++++++++++++---- 7 files changed, 353 insertions(+), 220 deletions(-) create mode 100644 script/deploys/DeployEtherFiWithdrawalBuffer.s.sol diff --git a/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol new file mode 100644 index 000000000..34f132e0c --- /dev/null +++ b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; + +import "@openzeppelin/contracts/utils/Strings.sol"; + +import "../../src/Liquifier.sol"; +import "../../src/EtherFiRestaker.sol"; +import "../../src/helpers/AddressProvider.sol"; +import "../../src/UUPSProxy.sol"; +import "../../src/EtherFiWithdrawalBuffer.sol"; + + +contract Deploy is Script { + using Strings for string; + AddressProvider public addressProvider; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address addressProviderAddress = vm.envAddress("CONTRACT_REGISTRY"); + addressProvider = AddressProvider(addressProviderAddress); + + vm.startBroadcast(deployerPrivateKey); + + EtherFiWithdrawalBuffer impl = new EtherFiWithdrawalBuffer( + addressProvider.getContractAddress("LiquidityPool"), + addressProvider.getContractAddress("EETH"), + addressProvider.getContractAddress("WeETH"), + 0x0c83EAe1FE72c390A02E426572854931EefF93BA, // protocol safe + 0x1d3Af47C1607A2EF33033693A9989D1d1013BB50 // role registry + ); + UUPSProxy proxy = new UUPSProxy(payable(impl), ""); + + EtherFiWithdrawalBuffer instance = EtherFiWithdrawalBuffer(payable(proxy)); + instance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + + vm.stopBroadcast(); + } +} diff --git a/script/deploys/DeployPhaseTwo.s.sol b/script/deploys/DeployPhaseTwo.s.sol index 2417e191b..c61d13feb 100644 --- a/script/deploys/DeployPhaseTwo.s.sol +++ b/script/deploys/DeployPhaseTwo.s.sol @@ -81,7 +81,7 @@ contract DeployPhaseTwoScript is Script { } retrieve_contract_addresses(); - withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0), 0); + withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0)); withdrawRequestNftProxy = new UUPSProxy(address(withdrawRequestNftImplementation), ""); withdrawRequestNftInstance = WithdrawRequestNFT(payable(withdrawRequestNftProxy)); diff --git a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol index 5862fb36e..115251475 100644 --- a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol +++ b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol @@ -20,7 +20,7 @@ contract WithdrawRequestNFTUpgrade is Script { vm.startBroadcast(deployerPrivateKey); WithdrawRequestNFT oracleInstance = WithdrawRequestNFT(proxyAddress); - WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(address(0), 0); + WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(address(0)); oracleInstance.upgradeTo(address(v2Implementation)); diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 439f0fa24..d3f6f29fe 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -38,9 +38,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad event HandledRemainderOfClaimedWithdrawRequests(uint256 eEthAmountToTreasury, uint256 eEthAmountBurnt); /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _treasury, uint16 _shareRemainderSplitToTreasuryInBps) { + constructor(address _treasury) { treasury = _treasury; - shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; _disableInitializers(); } diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol index d73268fd0..5073ed989 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -11,29 +11,30 @@ contract EtherFiWithdrawalBufferTest is TestSetup { 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 setUp_Fork() public { initializeRealisticFork(MAINNET_FORK); - vm.startPrank(owner); + vm.startPrank(roleRegistry.owner()); roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), op_admin); vm.stopPrank(); etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(1e4, 1_00, 10_00); // 10% fee split to treasury, 1% exit fee, 10% low watermark + etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + + _upgrade_liquidity_pool_contract(); + + vm.prank(liquidityPoolInstance.owner()); + liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); } function test_rate_limit() public { + vm.deal(user, 1000 ether); + vm.prank(user); + liquidityPoolInstance.deposit{value: 1000 ether}(); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(1 ether), true); assertEq(etherFiWithdrawalBufferInstance.canRedeem(5 ether - 1), true); assertEq(etherFiWithdrawalBufferInstance.canRedeem(5 ether + 1), false); @@ -58,132 +59,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { 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), 5 ether); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); - - vm.stopPrank(); - } - - function test_mainnet_redeem_weEth_with_rebase() public { - vm.deal(user, 100 ether); - - vm.startPrank(user); - liquidityPoolInstance.deposit{value: 10 ether}(); - eETHInstance.approve(address(weEthInstance), 10 ether); - weEthInstance.wrap(10 ether); - vm.stopPrank(); - - uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; - - vm.prank(address(membershipManagerInstance)); - liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 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.0101 ether); - assertEq(address(user).balance, userBalance + 0.9999 ether); - vm.stopPrank(); - } - - function test_redeem_weEth_1() 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); - - eETHInstance.approve(address(weEthInstance), 6 ether); - weEthInstance.wrap(6 ether); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemWeEth(5 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(); + vm.expectRevert("INVALID"); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% } - // The test ensures that: - // - Redemption works correctly within allowed limits. - // - Fees are applied accurately. - // - The function properly reverts when redemption conditions aren't met. function testFuzz_redeemEEth( uint256 depositAmount, uint256 redeemAmount, @@ -214,13 +94,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); vm.startPrank(user); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); - - if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; @@ -230,12 +108,12 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertApproxEqAbs( eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalanceBefore + treasuryFee, - 1e1 + 1e2 ); assertApproxEqAbs( address(user).balance, userBalanceBefore + userReceives, - 1e1 + 1e2 ); } else { @@ -248,16 +126,18 @@ contract EtherFiWithdrawalBufferTest is TestSetup { function testFuzz_redeemWeEth( uint256 depositAmount, uint256 redeemAmount, - uint256 exitFeeSplitBps, + uint16 exitFeeSplitBps, + int256 rebase, uint16 exitFeeBps, uint16 lowWatermarkBps ) public { // Bound the parameters depositAmount = bound(depositAmount, 1 ether, 1000 ether); redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); - exitFeeSplitBps = bound(exitFeeSplitBps, 0, 10000); - exitFeeBps = uint16(bound(uint256(exitFeeBps), 0, 10000)); - lowWatermarkBps = uint16(bound(uint256(lowWatermarkBps), 0, 10000)); + exitFeeSplitBps = uint16(bound(exitFeeSplitBps, 0, 10000)); + exitFeeBps = uint16(bound(exitFeeBps, 0, 10000)); + lowWatermarkBps = uint16(bound(lowWatermarkBps, 0, 10000)); + rebase = bound(rebase, 0, int128(uint128(depositAmount) / 10)); // Deal Ether to user and perform deposit vm.deal(user, depositAmount); @@ -265,6 +145,10 @@ contract EtherFiWithdrawalBufferTest is TestSetup { liquidityPoolInstance.deposit{value: depositAmount}(); vm.stopPrank(); + // Apply rebase + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(int128(rebase)); + // Set fee and watermark configurations vm.prank(owner); etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); @@ -275,35 +159,39 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.prank(owner); etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); - // User approves weETH and attempts redemption + // Convert redeemAmount from ETH to weETH vm.startPrank(user); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + eETHInstance.approve(address(weEthInstance), redeemAmount); + weEthInstance.wrap(redeemAmount); + uint256 weEthAmount = weEthInstance.balanceOf(user); - if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); - etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); - uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), weEthAmount); + etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + + uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; - uint256 userReceives = redeemAmount - totalFee; + uint256 userReceives = eEthAmount - totalFee; assertApproxEqAbs( eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalanceBefore + treasuryFee, - 1e1 + 1e2 ); assertApproxEqAbs( address(user).balance, userBalanceBefore + userReceives, - 1e1 + 1e2 ); } else { vm.expectRevert(); - etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); } vm.stopPrank(); } @@ -380,22 +268,26 @@ contract EtherFiWithdrawalBufferTest is TestSetup { } function test_mainnet_redeem_eEth() public { + setUp_Fork(); + 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}(); + liquidityPoolInstance.deposit{value: 10 ether}(); + uint256 redeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); - assertEq(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + 0.01 ether); - assertEq(address(user).balance, userBalance + 0.99 ether); + uint256 totalFee = (1 ether * 1e2) / 1e4; + uint256 treasuryFee = (totalFee * 1e3) / 1e4; + uint256 userReceives = 1 ether - totalFee; + + assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); @@ -403,4 +295,64 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.stopPrank(); } + + function test_mainnet_redeem_weEth_with_rebase() public { + setUp_Fork(); + + vm.deal(user, 100 ether); + + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(weEthInstance), 10 ether); + weEthInstance.wrap(1 ether); + vm.stopPrank(); + + uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; + + vm.prank(address(membershipManagerV1Instance)); + liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 10 eETH earned 1 ETH + + vm.startPrank(user); + uint256 weEthAmount = weEthInstance.balanceOf(user); + uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + + uint256 totalFee = (eEthAmount * 1e2) / 1e4; + uint256 treasuryFee = (totalFee * 1e3) / 1e4; + uint256 userReceives = eEthAmount - totalFee; + + assertApproxEqAbs(eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); + + vm.stopPrank(); + } + + function test_mainnet_redeem_beyond_liquidity_fails() public { + setUp_Fork(); + + uint256 redeemAmount = liquidityPoolInstance.getTotalPooledEther() / 2; + vm.prank(address(liquidityPoolInstance)); + eETHInstance.mintShares(user, 2 * redeemAmount); + + vm.startPrank(op_admin); + etherFiWithdrawalBufferInstance.setCapacity(2 * redeemAmount); + etherFiWithdrawalBufferInstance.setRefillRatePerSecond(2 * redeemAmount); + vm.stopPrank(); + + vm.warp(block.timestamp + 1); + + vm.startPrank(user); + + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); + vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + + vm.stopPrank(); + } } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 9c9fe030c..af6a4ca82 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -559,7 +559,7 @@ contract TestSetup is Test { membershipNftProxy = new UUPSProxy(address(membershipNftImplementation), ""); membershipNftInstance = MembershipNFT(payable(membershipNftProxy)); - withdrawRequestNFTImplementation = new WithdrawRequestNFT(address(0), 0); + withdrawRequestNFTImplementation = new WithdrawRequestNFT(address(treasuryInstance)); withdrawRequestNFTProxy = new UUPSProxy(address(withdrawRequestNFTImplementation), ""); withdrawRequestNFTInstance = WithdrawRequestNFT(payable(withdrawRequestNFTProxy)); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 924545421..816d65424 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -14,60 +14,6 @@ contract WithdrawRequestNFTTest is TestSetup { setUpTests(); } - function test_WithdrawRequestNftInitializedCorrectly() public { - assertEq(address(withdrawRequestNFTInstance.liquidityPool()), address(liquidityPoolInstance)); - assertEq(address(withdrawRequestNFTInstance.eETH()), address(eETHInstance)); - } - - function test_RequestWithdraw() public { - startHoax(bob); - liquidityPoolInstance.deposit{value: 10 ether}(); - vm.stopPrank(); - - assertEq(liquidityPoolInstance.getTotalPooledEther(), 10 ether); - assertEq(eETHInstance.balanceOf(address(bob)), 10 ether); - - uint96 amountOfEEth = 1 ether; - - vm.prank(bob); - eETHInstance.approve(address(liquidityPoolInstance), amountOfEEth); - - vm.prank(bob); - uint256 requestId = liquidityPoolInstance.requestWithdraw(bob, amountOfEEth); - - WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); - - assertEq(request.amountOfEEth, 1 ether, "Amount of eEth should match"); - assertEq(request.shareOfEEth, 1 ether, "Share of eEth should match"); - assertTrue(request.isValid, "Request should be valid"); - } - - function test_RequestIdIncrements() public { - startHoax(bob); - liquidityPoolInstance.deposit{value: 10 ether}(); - vm.stopPrank(); - - assertEq(liquidityPoolInstance.getTotalPooledEther(), 10 ether); - - uint96 amountOfEEth = 1 ether; - - vm.prank(bob); - eETHInstance.approve(address(liquidityPoolInstance), amountOfEEth); - - vm.prank(bob); - uint256 requestId1 = liquidityPoolInstance.requestWithdraw(bob, amountOfEEth); - - assertEq(requestId1, 1, "Request id should be 1"); - - vm.prank(bob); - eETHInstance.approve(address(liquidityPoolInstance), amountOfEEth); - - vm.prank(bob); - uint256 requestId2 = liquidityPoolInstance.requestWithdraw(bob, amountOfEEth); - - assertEq(requestId2, 2, "Request id should be 2"); - } - function test_finalizeRequests() public { startHoax(bob); liquidityPoolInstance.deposit{value: 10 ether}(); @@ -183,6 +129,7 @@ contract WithdrawRequestNFTTest is TestSetup { assertEq(eETHInstance.balanceOf(bob), 9 ether); assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 1 ether, "eETH balance should be 1 ether"); + assertEq(eETHInstance.balanceOf(address(treasuryInstance)), 0 ether, "Treasury balance should be 0 ether"); // Rebase with accrued_rewards = 10 ether for the deposited 10 ether // -> 1 ether eETH shares = 2 ether ETH @@ -202,7 +149,7 @@ contract WithdrawRequestNFTTest is TestSetup { uint256 bobsEndingBalance = address(bob).balance; 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(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); } function test_ValidClaimWithdrawWithNegativeRebase() public { @@ -418,8 +365,203 @@ contract WithdrawRequestNFTTest is TestSetup { initializeRealisticFork(MAINNET_FORK); vm.startPrank(withdrawRequestNFTInstance.owner()); - withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner), 50_00))); + withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); + + withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds); } + + function testFuzz_RequestWithdraw(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { + // Assume valid conditions + vm.assume(depositAmount >= 1 ether && depositAmount <= 1000 ether); + vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount); + vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance)); + + // Setup initial balance for bob + vm.deal(bob, depositAmount); + + // Deposit ETH and get eETH + vm.startPrank(bob); + liquidityPoolInstance.deposit{value: depositAmount}(); + + // Approve and request withdraw + eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); + uint256 requestId = liquidityPoolInstance.requestWithdraw(recipient, withdrawAmount); + vm.stopPrank(); + + // Verify the request was created correctly + WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); + + assertEq(request.amountOfEEth, withdrawAmount, "Incorrect withdrawal amount"); + assertEq(request.shareOfEEth, liquidityPoolInstance.sharesForAmount(withdrawAmount), "Incorrect share amount"); + assertTrue(request.isValid, "Request should be valid"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), recipient, "Incorrect NFT owner"); + + // Verify eETH balances + assertEq(eETHInstance.balanceOf(bob), depositAmount - withdrawAmount, "Incorrect remaining eETH balance"); + assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), withdrawAmount, "Incorrect contract eETH balance"); + assertEq(withdrawRequestNFTInstance.nextRequestId(), requestId + 1, "Incorrect next request ID"); + + if (eETHInstance.balanceOf(bob) > 0) { + uint256 reqAmount = eETHInstance.balanceOf(bob); + vm.startPrank(bob); + eETHInstance.approve(address(liquidityPoolInstance), reqAmount); + uint256 requestId2 = liquidityPoolInstance.requestWithdraw(recipient, reqAmount); + vm.stopPrank(); + assertEq(requestId2, requestId + 1, "Incorrect next request ID"); + } + } + + function testFuzz_ClaimWithdraw( + uint96 depositAmount, + uint96 withdrawAmount, + uint96 rebaseAmount, + uint16 remainderSplitBps, + address recipient + ) public { + // Assume valid conditions + vm.assume(depositAmount >= 1 ether && depositAmount <= 1e6 ether); + vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount); + vm.assume(rebaseAmount >= 0 && rebaseAmount <= depositAmount); + vm.assume(remainderSplitBps <= 10000); + vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance)); + + // Setup initial balance for recipient + vm.deal(recipient, depositAmount); + + // Configure remainder split + vm.prank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(remainderSplitBps); + + // First deposit ETH to get eETH + vm.startPrank(recipient); + liquidityPoolInstance.deposit{value: depositAmount}(); + + // Record initial balances + uint256 treasuryEEthBefore = eETHInstance.balanceOf(address(treasuryInstance)); + uint256 recipientBalanceBefore = address(recipient).balance; + + // Request withdraw + eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); + uint256 requestId = liquidityPoolInstance.requestWithdraw(recipient, withdrawAmount); + vm.stopPrank(); + + // Get initial request state + WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); + + // Simulate rebase after request but before claim + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(int128(uint128(rebaseAmount))); + + // Calculate expected withdrawal amounts after rebase + uint256 sharesValue = liquidityPoolInstance.amountForShare(request.shareOfEEth); + uint256 expectedWithdrawAmount = withdrawAmount < sharesValue ? withdrawAmount : sharesValue; + uint256 unusedShares = request.shareOfEEth - liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); + uint256 expectedTreasuryShares = (unusedShares * remainderSplitBps) / 10000; + uint256 expectedBurnedShares = request.shareOfEEth - expectedTreasuryShares; + assertGe(unusedShares, 0, "Unused shares should be non-negative because there was positive rebase"); + + // Track initial shares and total supply + uint256 initialTotalShares = eETHInstance.totalShares(); + + _finalizeWithdrawalRequest(requestId); + + vm.prank(recipient); + withdrawRequestNFTInstance.claimWithdraw(requestId); + + // Calculate expected burnt shares + uint256 burnedShares = initialTotalShares - eETHInstance.totalShares(); + + // Verify share burning + assertApproxEqAbs( + burnedShares, + expectedBurnedShares, + 1e1, + "Incorrect amount of shares burnt" + ); + assertLe(burnedShares, request.shareOfEEth, "Burned shares should be less than or equal to requested shares"); + + // Verify total supply reduction + assertApproxEqAbs( + eETHInstance.totalShares(), + initialTotalShares - burnedShares, + 1, + "Total shares not reduced correctly" + ); + assertGe( + eETHInstance.totalShares(), + initialTotalShares - burnedShares, + "Total shares should be greater than or equal to initial shares minus burned shares" + ); + + // Verify the withdrawal results + WithdrawRequestNFT.WithdrawRequest memory requestAfter = withdrawRequestNFTInstance.getRequest(requestId); + + // Request should be cleared + assertEq(requestAfter.amountOfEEth, 0, "Request should be cleared after claim"); + + // NFT should be burned + vm.expectRevert("ERC721: invalid token ID"); + withdrawRequestNFTInstance.ownerOf(requestId); + + // Calculate and verify remainder splitting + if (unusedShares > 0) { + assertApproxEqAbs( + eETHInstance.balanceOf(address(treasuryInstance)) - treasuryEEthBefore, + liquidityPoolInstance.amountForShare(expectedTreasuryShares), + 1e1, + "Incorrect treasury eETH balance" + ); + } + + // Verify recipient received correct ETH amount + assertEq( + address(recipient).balance, + recipientBalanceBefore + expectedWithdrawAmount, + "Recipient should receive correct ETH amount" + ); + } + + function testFuzz_InvalidateRequest(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { + // Assume valid conditions + vm.assume(depositAmount >= 1 ether && depositAmount <= 1000 ether); + vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount); + vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance) && !withdrawRequestNFTInstance.admins(recipient)); + + // Setup initial balance and deposit + vm.deal(recipient, depositAmount); + + vm.startPrank(recipient); + liquidityPoolInstance.deposit{value: depositAmount}(); + + // Request withdraw + eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); + uint256 requestId = liquidityPoolInstance.requestWithdraw(recipient, withdrawAmount); + vm.stopPrank(); + + // Verify request is initially valid + assertTrue(withdrawRequestNFTInstance.isValid(requestId), "Request should start valid"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), recipient, "Recipient should own NFT"); + + // Non-admin cannot invalidate + vm.prank(recipient); + vm.expectRevert("Caller is not the admin"); + withdrawRequestNFTInstance.invalidateRequest(requestId); + + // Admin invalidates request + vm.prank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.updateAdmin(recipient, true); + vm.prank(recipient); + withdrawRequestNFTInstance.invalidateRequest(requestId); + + // Verify request state after invalidation + assertFalse(withdrawRequestNFTInstance.isValid(requestId), "Request should be invalid"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), recipient, "NFT ownership should remain unchanged"); + + // Verify cannot transfer invalid request + vm.prank(recipient); + vm.expectRevert("INVALID_REQUEST"); + withdrawRequestNFTInstance.transferFrom(recipient, address(0xdead), requestId); + } } From c377e8e13d13661b35d63148598aa56ffb73b863 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 20 Dec 2024 10:24:20 +0900 Subject: [PATCH 06/95] handle issues in calculating the dust shares --- src/WithdrawRequestNFT.sol | 10 ++++++++-- test/WithdrawRequestNFT.t.sol | 27 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index d3f6f29fe..03e9b8b28 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -131,7 +131,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // 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 { + function handleAccumulatedShareRemainder(uint256[] memory _reqIds, uint256 _scanBegin) external onlyOwner { + assert (_scanBegin < nextRequestId); + bytes32 slot = keccak256("handleAccumulatedShareRemainder"); uint256 executed; assembly { @@ -141,9 +143,13 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint256 eEthSharesUnclaimedYet = 0; for (uint256 i = 0; i < _reqIds.length; i++) { - assert (_requests[_reqIds[i]].isValid); + if (!_requests[_reqIds[i]].isValid) continue; eEthSharesUnclaimedYet += _requests[_reqIds[i]].shareOfEEth; } + for (uint256 i = _scanBegin + 1; i < nextRequestId; i++) { + if (!_requests[i].isValid) continue; + eEthSharesUnclaimedYet += _requests[i].shareOfEEth; + } uint256 eEthSharesRemainder = eETH.shares(address(this)) - eEthSharesUnclaimedYet; handleRemainder(eEthSharesRemainder); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 816d65424..ed2c584d7 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -368,8 +368,31 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); - - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds); + vm.stopPrank(); + + // The goal is to count ALL dust shares that could be burnt in the past if we had the feature. + // Option 1 is to perform the off-chain calculation and input it as a parameter to the function, which is less transparent and not ideal + // Option 2 is to perform the calculation on-chain, which is more transparent but would require a lot of gas iterating for all CLAIMED requests + // -> The idea is to calculate the total eETH shares of ALL UNCLAIMED requests. + // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all CLAIMED requests. + // -> eETH.share(withdrawRequsetNFT) - Sum(request.shareOfEEth) for ALL unclaimed + + // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain: + // 1. When we queue up the txn, we will take a snapshot of ALL unclaimed requests and put their IDs as a parameter. + // 2. (issue) during the timelock period, there will be new requests that can't be included in the snapshot. + // the idea is to input last finalized request ID and scan from there to the latest request ID on-chain + uint256 scanBegin = withdrawRequestNFTInstance.lastFinalizedRequestId(); + + // If the request gets claimed during the timelock period, it will get skipped in the calculation. + vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[0])); + withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); + + vm.startPrank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); + vm.stopPrank(); + + vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[1])); + withdrawRequestNFTInstance.claimWithdraw(reqIds[1]); } function testFuzz_RequestWithdraw(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { From a3aeac94b565830ddb0a0bc1f450d4d7662768a9 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 20 Dec 2024 10:26:11 +0900 Subject: [PATCH 07/95] improve comments --- test/WithdrawRequestNFT.t.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index ed2c584d7..939d4a705 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -374,10 +374,14 @@ contract WithdrawRequestNFTTest is TestSetup { // Option 1 is to perform the off-chain calculation and input it as a parameter to the function, which is less transparent and not ideal // Option 2 is to perform the calculation on-chain, which is more transparent but would require a lot of gas iterating for all CLAIMED requests // -> The idea is to calculate the total eETH shares of ALL UNCLAIMED requests. - // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all CLAIMED requests. + // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all UNCLAIMED requests. // -> eETH.share(withdrawRequsetNFT) - Sum(request.shareOfEEth) for ALL unclaimed - // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain: + // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain. + // One way is to iterate through all requests and sum up the shareOfEEth for all unclaimed requests. + // However, this would require a lot of gas and is not ideal. + // + // The idea is: // 1. When we queue up the txn, we will take a snapshot of ALL unclaimed requests and put their IDs as a parameter. // 2. (issue) during the timelock period, there will be new requests that can't be included in the snapshot. // the idea is to input last finalized request ID and scan from there to the latest request ID on-chain From 57bd0fc7d379183a2ee82001debcea5c733afb66 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 24 Dec 2024 09:27:38 +0900 Subject: [PATCH 08/95] add sorted & unique constraints + type change to reduce gas --- src/WithdrawRequestNFT.sol | 7 ++++++- test/WithdrawRequestNFT.t.sol | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 03e9b8b28..2a1da3b57 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -131,7 +131,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // 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, uint256 _scanBegin) external onlyOwner { + function handleAccumulatedShareRemainder(uint32[] memory _reqIds, uint256 _scanBegin) external onlyOwner { assert (_scanBegin < nextRequestId); bytes32 slot = keccak256("handleAccumulatedShareRemainder"); @@ -141,6 +141,11 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } require(executed == 0, "ALREADY_EXECUTED"); + // Check that _reqIds are sorted in ascending order and there is no duplication + for (uint256 i = 1; i < _reqIds.length; i++) { + require(_reqIds[i] > _reqIds[i - 1], "Entries must be sorted and unique"); + } + uint256 eEthSharesUnclaimedYet = 0; for (uint256 i = 0; i < _reqIds.length; i++) { if (!_requests[_reqIds[i]].isValid) continue; diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 939d4a705..f7d7ea8c6 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -8,7 +8,7 @@ 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]; + uint32[] public reqIds =[ 20, 388, 478, 714, 726, 729, 735, 815, 861, 916, 941, 1014, 1067, 1154, 1194, 1253]; function setUp() public { setUpTests(); @@ -392,6 +392,24 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); vm.startPrank(withdrawRequestNFTInstance.owner()); + uint32[] memory reqIdsWithIssues = new uint32[](4); + reqIdsWithIssues[0] = reqIds[0]; + reqIdsWithIssues[1] = reqIds[1]; + reqIdsWithIssues[2] = reqIds[3]; + reqIdsWithIssues[3] = reqIds[2]; + vm.expectRevert(); + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); + + reqIdsWithIssues[0] = reqIds[0]; + reqIdsWithIssues[1] = reqIds[1]; + reqIdsWithIssues[2] = reqIds[2]; + reqIdsWithIssues[3] = reqIds[2]; + vm.expectRevert(); + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); + vm.stopPrank(); + + + vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); vm.stopPrank(); From b6100b64935a8dcbb1a430fb1b213a17cf06f717 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 24 Dec 2024 10:05:15 +0900 Subject: [PATCH 09/95] update 'handleAccumulatedShareRemainder' to be callable by admin --- src/WithdrawRequestNFT.sol | 2 +- test/WithdrawRequestNFT.t.sol | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 2a1da3b57..58b851be8 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -131,7 +131,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // 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(uint32[] memory _reqIds, uint256 _scanBegin) external onlyOwner { + function handleAccumulatedShareRemainder(uint32[] memory _reqIds, uint256 _scanBegin) external onlyAdmin { assert (_scanBegin < nextRequestId); bytes32 slot = keccak256("handleAccumulatedShareRemainder"); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index f7d7ea8c6..4bd8bc627 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -364,6 +364,8 @@ contract WithdrawRequestNFTTest is TestSetup { function test_distributeImplicitFee() public { initializeRealisticFork(MAINNET_FORK); + address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); @@ -391,7 +393,7 @@ contract WithdrawRequestNFTTest is TestSetup { vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[0])); withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); - vm.startPrank(withdrawRequestNFTInstance.owner()); + vm.startPrank(etherfi_admin_wallet); uint32[] memory reqIdsWithIssues = new uint32[](4); reqIdsWithIssues[0] = reqIds[0]; reqIdsWithIssues[1] = reqIds[1]; @@ -406,10 +408,7 @@ contract WithdrawRequestNFTTest is TestSetup { reqIdsWithIssues[3] = reqIds[2]; vm.expectRevert(); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); - vm.stopPrank(); - - vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); vm.stopPrank(); From e8734d6e6ed5faea0a41980128c47283c7970d3e Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 26 Dec 2024 15:57:58 +0900 Subject: [PATCH 10/95] Certora audit: (1) add {aggregateSumEEthShareAmount}, (2) fix {_claimWithdraw} to account with 'totalLockedEEthShares', (3) simplify 'seizeRequest', (4) handle some issues --- src/EtherFiWithdrawalBuffer.sol | 32 +++--- src/WithdrawRequestNFT.sol | 165 +++++++++++++++-------------- test/EtherFiWithdrawalBuffer.t.sol | 9 ++ test/WithdrawRequestNFT.t.sol | 95 ++++++----------- 4 files changed, 143 insertions(+), 158 deletions(-) diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index a79b9e948..b51ed45b4 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -49,6 +49,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint16 public exitFeeInBps; uint16 public lowWatermarkInBpsOfTvl; // bps of TVL + event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); + receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor @@ -79,9 +81,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param eEthAmount The amount of eETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. * @param owner The address of the owner of the eETH. - * @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) { + function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); @@ -90,7 +91,7 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 afterEEthAmount = eEth.balanceOf(address(this)); uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - return _redeem(transferredEEthAmount, receiver); + _redeem(transferredEEthAmount, receiver); } /** @@ -98,9 +99,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param weEthAmount The amount of weETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. * @param owner The address of the owner of the weETH. - * @return The amount of ETH sent to the receiver and the exit fee amount. */ - function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { + function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { uint256 eEthShares = weEthAmount; uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); @@ -112,7 +112,7 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 afterEEthAmount = eEth.balanceOf(address(this)); uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - return _redeem(transferredEEthAmount, receiver); + _redeem(transferredEEthAmount, receiver); } @@ -120,9 +120,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @notice Redeems ETH. * @param ethAmount The amount of ETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. - * @return The amount of ETH sent to the receiver and the exit fee amount. */ - function _redeem(uint256 ethAmount, address receiver) internal returns (uint256, uint256) { + function _redeem(uint256 ethAmount, address receiver) internal { _updateRateLimit(ethAmount); uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); @@ -130,16 +129,18 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShareToReceiver); uint256 prevLpBalance = address(liquidityPool).balance; - uint256 prevBalance = address(this).balance; - uint256 burnedShares = (eEthAmountToReceiver > 0) ? liquidityPool.withdraw(address(this), eEthAmountToReceiver) : 0; - uint256 ethReceived = address(this).balance - prevBalance; + uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); - uint256 ethShareFee = ethShares - burnedShares; - uint256 eEthAmountFee = liquidityPool.amountForShare(ethShareFee); + uint256 ethShareFee = ethShares - sharesToBurn; uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + // Withdraw ETH from the liquidity pool + uint256 prevBalance = address(this).balance; + assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); + uint256 ethReceived = address(this).balance - prevBalance; + // To Stakers by burning shares eEth.burnShares(address(this), feeShareToStakers); @@ -148,9 +149,10 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); - require(success && address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Transfer failed"); + require(success, "EtherFiWithdrawalBuffer: Transfer failed"); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Invalid liquidity pool balance"); - return (ethReceived, eEthAmountFee); + emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } /** diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 58b851be8..91fa86325 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -30,13 +30,25 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 public lastFinalizedRequestId; uint16 public shareRemainderSplitToTreasuryInBps; + // inclusive + uint32 private _currentRequestIdToScanFromForShareRemainder; + uint32 private _lastRequestIdToScanUntilForShareRemainder; + + uint256 public totalLockedEEthShares; + + bool public paused; + address public pauser; + 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); + event Paused(address account); + event Unpaused(address account); + + /// @custom:oz-upgrades-unsafe-allow constructor constructor(address _treasury) { treasury = _treasury; @@ -57,6 +69,14 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad nextRequestId = 1; } + function initializeOnUpgrade(address _pauser) external onlyOwner { + paused = false; + pauser = _pauser; + + _currentRequestIdToScanFromForShareRemainder = 1; + _lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; + } + /// @notice creates a withdraw request and issues an associated NFT to the recipient /// @dev liquidity pool contract will call this function when a user requests withdraw /// @param amountOfEEth amount of eETH requested for withdrawal @@ -64,13 +84,15 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param recipient address to recieve with WithdrawRequestNFT /// @param fee fee to be subtracted from amount when recipient calls claimWithdraw /// @return uint256 id of the withdraw request - function requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) external payable onlyLiquidtyPool returns (uint256) { + function requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) external payable onlyLiquidtyPool whenNotPaused returns (uint256) { uint256 requestId = nextRequestId++; uint32 feeGwei = uint32(fee / 1 gwei); _requests[requestId] = IWithdrawRequestNFT.WithdrawRequest(amountOfEEth, shareOfEEth, true, feeGwei); _safeMint(recipient, requestId); + totalLockedEEthShares += shareOfEEth; + emit WithdrawRequestCreated(uint32(requestId), amountOfEEth, shareOfEEth, recipient, fee); return requestId; } @@ -93,7 +115,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @notice called by the NFT owner to claim their ETH /// @dev burns the NFT and transfers ETH from the liquidity pool to the owner minus any fee, withdraw request must be valid and finalized /// @param tokenId the id of the withdraw request and associated NFT - function claimWithdraw(uint256 tokenId) external { + function claimWithdraw(uint256 tokenId) external whenNotPaused { return _claimWithdraw(tokenId, ownerOf(tokenId)); } @@ -102,91 +124,48 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad IWithdrawRequestNFT.WithdrawRequest memory request = _requests[tokenId]; require(request.isValid, "Request is not valid"); - uint256 fee = uint256(request.feeGwei) * 1 gwei; uint256 amountToWithdraw = getClaimableAmount(tokenId); // transfer eth to recipient _burn(tokenId); delete _requests[tokenId]; + + uint256 shareAmountToBurnForWithdrawal = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); + totalLockedEEthShares -= shareAmountToBurnForWithdrawal; - uint256 amountBurnedShare = 0; - if (fee > 0) { - amountBurnedShare += liquidityPool.withdraw(address(membershipManager), fee); - } - amountBurnedShare += liquidityPool.withdraw(recipient, amountToWithdraw); - uint256 amountUnBurnedShare = request.shareOfEEth - amountBurnedShare; - handleRemainder(amountUnBurnedShare); + uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); + assert (amountBurnedShare == shareAmountToBurnForWithdrawal); - emit WithdrawRequestClaimed(uint32(tokenId), amountToWithdraw + fee, amountBurnedShare, recipient, fee); + emit WithdrawRequestClaimed(uint32(tokenId), amountToWithdraw, amountBurnedShare, recipient, 0); } - function batchClaimWithdraw(uint256[] calldata tokenIds) external { + function batchClaimWithdraw(uint256[] calldata tokenIds) external whenNotPaused { for (uint256 i = 0; i < tokenIds.length; i++) { _claimWithdraw(tokenIds[i], ownerOf(tokenIds[i])); } } - // 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(uint32[] memory _reqIds, uint256 _scanBegin) external onlyAdmin { - assert (_scanBegin < nextRequestId); - - bytes32 slot = keccak256("handleAccumulatedShareRemainder"); - uint256 executed; - assembly { - executed := sload(slot) - } - require(executed == 0, "ALREADY_EXECUTED"); + // This function is used to aggregate the sum of the eEth shares of the requests that have not been claimed yet. + // To be triggered during the upgrade to the new version of the contract. + function aggregateSumEEthShareAmount(uint256 _numReqsToScan) external { + // [scanFrom, scanUntil] + uint256 scanFrom = _currentRequestIdToScanFromForShareRemainder; + uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); - // Check that _reqIds are sorted in ascending order and there is no duplication - for (uint256 i = 1; i < _reqIds.length; i++) { - require(_reqIds[i] > _reqIds[i - 1], "Entries must be sorted and unique"); - } - - uint256 eEthSharesUnclaimedYet = 0; - for (uint256 i = 0; i < _reqIds.length; i++) { - if (!_requests[_reqIds[i]].isValid) continue; - eEthSharesUnclaimedYet += _requests[_reqIds[i]].shareOfEEth; - } - for (uint256 i = _scanBegin + 1; i < nextRequestId; i++) { + for (uint256 i = scanFrom; i <= scanUntil; i++) { if (!_requests[i].isValid) continue; - eEthSharesUnclaimedYet += _requests[i].shareOfEEth; + totalLockedEEthShares += _requests[i].shareOfEEth; } - uint256 eEthSharesRemainder = eETH.shares(address(this)) - eEthSharesUnclaimedYet; - handleRemainder(eEthSharesRemainder); - - assembly { - sstore(slot, 1) - executed := sload(slot) - } - assert (executed == 1); + _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); } - // Given an invalidated withdrawal request NFT of ID `requestId`:, - // - burn the NFT - // - withdraw its ETH to the `recipient` - function seizeInvalidRequest(uint256 requestId, address recipient) external onlyOwner { + // Seize the request simply by transferring it to another recipient + function seizeRequest(uint256 requestId, address recipient) external onlyOwner { require(!_requests[requestId].isValid, "Request is valid"); require(ownerOf(requestId) != address(0), "Already Claimed"); - // Bring the NFT to the `msg.sender` == contract owner - _transfer(ownerOf(requestId), owner(), requestId); - - // Undo its invalidation to claim - _requests[requestId].isValid = true; - - // its ETH amount is not locked - // - if it was finalized when being invalidated, we revoked it via `reduceEthAmountLockedForWithdrawal` - // - if it was not finalized when being invalidated, it was not locked - uint256 ethAmount = getClaimableAmount(requestId); - liquidityPool.addEthAmountLockedForWithdrawal(uint128(ethAmount)); - - // withdraw the ETH to the recipient - _claimWithdraw(requestId, recipient); + _transfer(ownerOf(requestId), recipient, requestId); emit WithdrawRequestSeized(uint32(requestId)); } @@ -221,13 +200,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad emit WithdrawRequestInvalidated(uint32(requestId)); } - function validateRequest(uint256 requestId) external onlyAdmin { - require(!_requests[requestId].isValid, "Request is valid"); - _requests[requestId].isValid = true; - - emit WithdrawRequestValidated(uint32(requestId)); - } - function updateAdmin(address _address, bool _isAdmin) external onlyOwner { require(_address != address(0), "Cannot be address zero"); admins[_address] = _isAdmin; @@ -237,30 +209,45 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; } + function pauseContract() external onlyPauser { + paused = true; + emit Paused(msg.sender); + } + + function unPauseContract() external onlyAdmin { + paused = false; + emit Unpaused(msg.sender); + } + /// @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); + /// @param _eEthAmount: the remainder of the eEth amount + function handleRemainder(uint256 _eEthAmount) external onlyAdmin { + require (getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); + + uint256 beforeEEthShares = eETH.shares(address(this)); + + uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(_eEthAmount); + uint256 eEthSharesToTreasury = eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); eETH.transfer(treasury, eEthAmountToTreasury); - uint256 eEthSharesToBurn = _eEthShares - eEthSharesToTreasury; + uint256 eEthSharesToBurn = eEthShares - eEthSharesToTreasury; eETH.burnShares(address(this), eEthSharesToBurn); + uint256 reducedEEthShares = beforeEEthShares - eETH.shares(address(this)); + totalLockedEEthShares -= reducedEEthShares; + 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++) { - uint256 tokenId = firstTokenId + i; - require(_requests[tokenId].isValid || msg.sender == owner(), "INVALID_REQUEST"); - } + function getEEthRemainderAmount() public view returns (uint256) { + uint256 eEthRemainderShare = eETH.shares(address(this)) - totalLockedEEthShares; + return liquidityPool.amountForShare(eEthRemainderShare); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} @@ -269,13 +256,27 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad return _getImplementation(); } + function _requireNotPaused() internal view virtual { + require(!paused, "Pausable: paused"); + } + modifier onlyAdmin() { require(admins[msg.sender], "Caller is not the admin"); _; } + modifier onlyPauser() { + require(msg.sender == pauser || admins[msg.sender] || msg.sender == owner(), "Caller is not the pauser"); + _; + } + modifier onlyLiquidtyPool() { require(msg.sender == address(liquidityPool), "Caller is not the liquidity pool"); _; } + + modifier whenNotPaused() { + _requireNotPaused(); + _; + } } diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol index 5073ed989..7aede9fcf 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -270,6 +270,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { function test_mainnet_redeem_eEth() public { setUp_Fork(); + vm.deal(alice, 50000 ether); + vm.prank(alice); + liquidityPoolInstance.deposit{value: 50000 ether}(); + + vm.deal(user, 100 ether); vm.startPrank(user); @@ -299,6 +304,10 @@ contract EtherFiWithdrawalBufferTest is TestSetup { function test_mainnet_redeem_weEth_with_rebase() public { setUp_Fork(); + vm.deal(alice, 50000 ether); + vm.prank(alice); + liquidityPoolInstance.deposit{value: 50000 ether}(); + vm.deal(user, 100 ether); vm.startPrank(user); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 4bd8bc627..07908ddf8 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -327,95 +327,68 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.transferFrom(alice, bob, requestId); } - function test_seizeInvalidAndMintNew_revert_if_not_owner() public { + function test_seizeRequest() public { uint256 requestId = test_InvalidatedRequestNft_after_finalization(); uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; // REVERT if not owner vm.prank(alice); vm.expectRevert("Ownable: caller is not the owner"); - withdrawRequestNFTInstance.seizeInvalidRequest(requestId, chad); - } - - function test_InvalidatedRequestNft_seizeInvalidAndMintNew_1() public { - uint256 requestId = test_InvalidatedRequestNft_after_finalization(); - uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; - uint256 chadBalance = address(chad).balance; + withdrawRequestNFTInstance.seizeRequest(requestId, chad); vm.prank(owner); - withdrawRequestNFTInstance.seizeInvalidRequest(requestId, chad); + withdrawRequestNFTInstance.seizeRequest(requestId, chad); assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); - assertEq(address(chad).balance, chadBalance + claimableAmount, "Chad should receive the claimable amount"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), chad, "Chad should own the NFT"); } - function test_InvalidatedRequestNft_seizeInvalidAndMintNew_2() public { - uint256 requestId = test_InvalidatedRequestNft_before_finalization(); - uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; - uint256 chadBalance = address(chad).balance; - - vm.prank(owner); - withdrawRequestNFTInstance.seizeInvalidRequest(requestId, chad); - - assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); - assertEq(address(chad).balance, chadBalance + claimableAmount, "Chad should receive the claimable amount"); - } - function test_distributeImplicitFee() public { + function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; vm.startPrank(withdrawRequestNFTInstance.owner()); + // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); - + withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet); withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); + withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); + + // 2. PAUSE + withdrawRequestNFTInstance.pauseContract(); vm.stopPrank(); - // The goal is to count ALL dust shares that could be burnt in the past if we had the feature. - // Option 1 is to perform the off-chain calculation and input it as a parameter to the function, which is less transparent and not ideal - // Option 2 is to perform the calculation on-chain, which is more transparent but would require a lot of gas iterating for all CLAIMED requests - // -> The idea is to calculate the total eETH shares of ALL UNCLAIMED requests. - // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all UNCLAIMED requests. - // -> eETH.share(withdrawRequsetNFT) - Sum(request.shareOfEEth) for ALL unclaimed - - // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain. - // One way is to iterate through all requests and sum up the shareOfEEth for all unclaimed requests. - // However, this would require a lot of gas and is not ideal. - // - // The idea is: - // 1. When we queue up the txn, we will take a snapshot of ALL unclaimed requests and put their IDs as a parameter. - // 2. (issue) during the timelock period, there will be new requests that can't be included in the snapshot. - // the idea is to input last finalized request ID and scan from there to the latest request ID on-chain - uint256 scanBegin = withdrawRequestNFTInstance.lastFinalizedRequestId(); - - // If the request gets claimed during the timelock period, it will get skipped in the calculation. - vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[0])); - withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); - - vm.startPrank(etherfi_admin_wallet); - uint32[] memory reqIdsWithIssues = new uint32[](4); - reqIdsWithIssues[0] = reqIds[0]; - reqIdsWithIssues[1] = reqIds[1]; - reqIdsWithIssues[2] = reqIds[3]; - reqIdsWithIssues[3] = reqIds[2]; - vm.expectRevert(); - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); - - reqIdsWithIssues[0] = reqIds[0]; - reqIdsWithIssues[1] = reqIds[1]; - reqIdsWithIssues[2] = reqIds[2]; - reqIdsWithIssues[3] = reqIds[2]; - vm.expectRevert(); - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); - - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); + vm.startPrank(etherfi_admin_wallet); + + // 3. AggSum + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); + // ... + + vm.stopPrank(); + + // 4. Unpause + vm.startPrank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.unPauseContract(); vm.stopPrank(); + // Back to normal vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[1])); withdrawRequestNFTInstance.claimWithdraw(reqIds[1]); } + function test_handleRemainder() public { + test_aggregateSumEEthShareAmount(); + + vm.startPrank(withdrawRequestNFTInstance.owner()); + + withdrawRequestNFTInstance.handleRemainder(1 ether); + + vm.stopPrank(); + } + function testFuzz_RequestWithdraw(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { // Assume valid conditions vm.assume(depositAmount >= 1 ether && depositAmount <= 1000 ether); From 71ffa8d25b4f7efe455decf005c1d67ba2d13561 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 30 Dec 2024 09:32:11 +0900 Subject: [PATCH 11/95] wip: to be amended --- src/WithdrawRequestNFT.sol | 8 ++++++-- test/WithdrawRequestNFT.t.sol | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 91fa86325..d858e827e 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -70,6 +70,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function initializeOnUpgrade(address _pauser) external onlyOwner { + require(pauser == address(0), "Already initialized"); + paused = false; pauser = _pauser; @@ -89,10 +91,10 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 feeGwei = uint32(fee / 1 gwei); _requests[requestId] = IWithdrawRequestNFT.WithdrawRequest(amountOfEEth, shareOfEEth, true, feeGwei); - _safeMint(recipient, requestId); - totalLockedEEthShares += shareOfEEth; + _safeMint(recipient, requestId); + emit WithdrawRequestCreated(uint32(requestId), amountOfEEth, shareOfEEth, recipient, fee); return requestId; } @@ -206,6 +208,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function updateShareRemainderSplitToTreasuryInBps(uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { + require(_shareRemainderSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; } @@ -227,6 +230,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { require (getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); + require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "Not all requests have been scanned"); uint256 beforeEEthShares = eETH.shares(address(this)); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 07908ddf8..2f5059976 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -458,6 +458,7 @@ contract WithdrawRequestNFTTest is TestSetup { // Record initial balances uint256 treasuryEEthBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 recipientBalanceBefore = address(recipient).balance; + uint256 initialTotalLockedEEthShares = withdrawRequestNFTInstance.totalLockedEEthShares(); // Request withdraw eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); @@ -467,6 +468,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Get initial request state WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); + assertEq(withdrawRequestNFTInstance.totalLockedEEthShares(), initialTotalLockedEEthShares + request.shareOfEEth, "Incorrect total locked shares"); + // Simulate rebase after request but before claim vm.prank(address(membershipManagerInstance)); liquidityPoolInstance.rebase(int128(uint128(rebaseAmount))); @@ -474,10 +477,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Calculate expected withdrawal amounts after rebase uint256 sharesValue = liquidityPoolInstance.amountForShare(request.shareOfEEth); uint256 expectedWithdrawAmount = withdrawAmount < sharesValue ? withdrawAmount : sharesValue; - uint256 unusedShares = request.shareOfEEth - liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); - uint256 expectedTreasuryShares = (unusedShares * remainderSplitBps) / 10000; - uint256 expectedBurnedShares = request.shareOfEEth - expectedTreasuryShares; - assertGe(unusedShares, 0, "Unused shares should be non-negative because there was positive rebase"); + uint256 expectedBurnedShares = liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); + uint256 expectedLockedShares = request.shareOfEEth - expectedBurnedShares; // Track initial shares and total supply uint256 initialTotalShares = eETHInstance.totalShares(); @@ -491,13 +492,14 @@ contract WithdrawRequestNFTTest is TestSetup { uint256 burnedShares = initialTotalShares - eETHInstance.totalShares(); // Verify share burning + assertLe(burnedShares, request.shareOfEEth, "Burned shares should be less than or equal to requested shares"); assertApproxEqAbs( burnedShares, expectedBurnedShares, - 1e1, + 1e3, "Incorrect amount of shares burnt" ); - assertLe(burnedShares, request.shareOfEEth, "Burned shares should be less than or equal to requested shares"); + // Verify total supply reduction assertApproxEqAbs( @@ -523,14 +525,12 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.ownerOf(requestId); // Calculate and verify remainder splitting - if (unusedShares > 0) { - assertApproxEqAbs( - eETHInstance.balanceOf(address(treasuryInstance)) - treasuryEEthBefore, - liquidityPoolInstance.amountForShare(expectedTreasuryShares), - 1e1, - "Incorrect treasury eETH balance" - ); - } + assertApproxEqAbs( + expectedLockedShares, + withdrawRequestNFTInstance.totalLockedEEthShares(), + 1e1, + "Incorrect locked eETH share" + ); // Verify recipient received correct ETH amount assertEq( From 9e0fb99c906c8921051e0518569372862073f754 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 30 Dec 2024 15:13:01 +0900 Subject: [PATCH 12/95] add simplified {invalidate, validate} request, fix unit tests --- src/WithdrawRequestNFT.sol | 33 ++++++++++++++++++-------- test/WithdrawRequestNFT.t.sol | 44 +++++++++-------------------------- 2 files changed, 34 insertions(+), 43 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index d858e827e..90b939b72 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -42,6 +42,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad 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); @@ -155,7 +156,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); for (uint256 i = scanFrom; i <= scanUntil; i++) { - if (!_requests[i].isValid) continue; + if (!_exists(i)) continue; totalLockedEEthShares += _requests[i].shareOfEEth; } @@ -165,7 +166,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // Seize the request simply by transferring it to another recipient function seizeRequest(uint256 requestId, address recipient) external onlyOwner { require(!_requests[requestId].isValid, "Request is valid"); - require(ownerOf(requestId) != address(0), "Already Claimed"); + require(_exists(requestId), "Request does not exist"); _transfer(ownerOf(requestId), recipient, requestId); @@ -181,7 +182,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function isValid(uint256 requestId) public view returns (bool) { - require(_exists(requestId), "Request does not exist"); + require(_exists(requestId), "Request does not exist11"); return _requests[requestId].isValid; } @@ -191,17 +192,19 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad function invalidateRequest(uint256 requestId) external onlyAdmin { require(isValid(requestId), "Request is not valid"); - - if (isFinalized(requestId)) { - uint256 ethAmount = getClaimableAmount(requestId); - liquidityPool.reduceEthAmountLockedForWithdrawal(uint128(ethAmount)); - } - _requests[requestId].isValid = false; emit WithdrawRequestInvalidated(uint32(requestId)); } + function validateRequest(uint256 requestId) external onlyAdmin { + require(_exists(requestId), "Request does not exist22"); + require(!_requests[requestId].isValid, "Request is valid"); + _requests[requestId].isValid = true; + + emit WithdrawRequestValidated(uint32(requestId)); + } + function updateAdmin(address _address, bool _isAdmin) external onlyOwner { require(_address != address(0), "Cannot be address zero"); admins[_address] = _isAdmin; @@ -229,7 +232,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require (getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); + require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "Not all requests have been scanned"); uint256 beforeEEthShares = eETH.shares(address(this)); @@ -254,6 +257,16 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad return liquidityPool.amountForShare(eEthRemainderShare); } + // the withdraw request NFT is transferrable + // - if the request is valid, it can be transferred by the owner of the NFT + // - if the request is invalid, it can be transferred only by the owner of the WithdarwRequestNFT contract + function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { + for (uint256 i = 0; i < batchSize; i++) { + uint256 tokenId = firstTokenId + i; + require(_requests[tokenId].isValid || msg.sender == owner(), "INVALID_REQUEST"); + } + } + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} function getImplementation() external view returns (address) { diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 2f5059976..7e3e62944 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -65,7 +65,7 @@ contract WithdrawRequestNFTTest is TestSetup { assertTrue(request.isValid, "Request should be valid"); } - function testInvalidClaimWithdraw() public { + function test_InvalidClaimWithdraw() public { startHoax(bob); liquidityPoolInstance.deposit{value: 10 ether}(); vm.stopPrank(); @@ -117,7 +117,7 @@ contract WithdrawRequestNFTTest is TestSetup { assertEq(liquidityPoolInstance.getTotalPooledEther(), 10 ether); assertEq(eETHInstance.balanceOf(bob), 10 ether); - assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); + assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should start from 0 ether"); // Case 1. // Even after the rebase, the withdrawal amount should remain the same; 1 eth @@ -149,7 +149,7 @@ contract WithdrawRequestNFTTest is TestSetup { uint256 bobsEndingBalance = address(bob).balance; assertEq(bobsEndingBalance, bobsStartingBalance + 1 ether, "Bobs balance should be 1 ether higher"); - assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); + assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 1 ether, "eETH balance should be 1 ether"); } function test_ValidClaimWithdrawWithNegativeRebase() public { @@ -319,31 +319,6 @@ contract WithdrawRequestNFTTest is TestSetup { _finalizeWithdrawalRequest(requestId); } - function test_InvalidatedRequestNft_NonTransferrable() public { - uint256 requestId = test_InvalidatedRequestNft_after_finalization(); - - vm.prank(alice); - vm.expectRevert("INVALID_REQUEST"); - withdrawRequestNFTInstance.transferFrom(alice, bob, requestId); - } - - function test_seizeRequest() public { - uint256 requestId = test_InvalidatedRequestNft_after_finalization(); - uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; - - // REVERT if not owner - vm.prank(alice); - vm.expectRevert("Ownable: caller is not the owner"); - withdrawRequestNFTInstance.seizeRequest(requestId, chad); - - vm.prank(owner); - withdrawRequestNFTInstance.seizeRequest(requestId, chad); - - assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); - assertEq(withdrawRequestNFTInstance.ownerOf(requestId), chad, "Chad should own the NFT"); - } - - function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); @@ -363,8 +338,7 @@ contract WithdrawRequestNFTTest is TestSetup { vm.startPrank(etherfi_admin_wallet); // 3. AggSum - withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); - withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); // ... vm.stopPrank(); @@ -383,7 +357,7 @@ contract WithdrawRequestNFTTest is TestSetup { test_aggregateSumEEthShareAmount(); vm.startPrank(withdrawRequestNFTInstance.owner()); - + vm.expectRevert("Not all requests have been scanned"); withdrawRequestNFTInstance.handleRemainder(1 ether); vm.stopPrank(); @@ -568,8 +542,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Admin invalidates request vm.prank(withdrawRequestNFTInstance.owner()); - withdrawRequestNFTInstance.updateAdmin(recipient, true); - vm.prank(recipient); + withdrawRequestNFTInstance.updateAdmin(admin, true); + vm.prank(admin); withdrawRequestNFTInstance.invalidateRequest(requestId); // Verify request state after invalidation @@ -580,5 +554,9 @@ contract WithdrawRequestNFTTest is TestSetup { vm.prank(recipient); vm.expectRevert("INVALID_REQUEST"); withdrawRequestNFTInstance.transferFrom(recipient, address(0xdead), requestId); + + // Owner can seize the invalidated request NFT + vm.prank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.seizeRequest(requestId, admin); } } From 98f483a2f968fb7f8d14b701af32b645f1bc885a Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 30 Dec 2024 18:09:49 +0900 Subject: [PATCH 13/95] rename EtherFiWithdrawBuffer -> EtherFiRedemptionManager --- script/deploys/DeployEtherFiRestaker.s.sol | 42 +++ .../DeployEtherFiWithdrawalBuffer.s.sol | 6 +- src/EtherFiRedemptionManager.sol | 280 ++++++++++++++++++ src/EtherFiWithdrawalBuffer.sol | 16 +- src/LiquidityPool.sol | 11 +- ...r.t.sol => EtherFiRedemptionManager.t.sol} | 136 ++++----- test/TestSetup.sol | 14 +- 7 files changed, 414 insertions(+), 91 deletions(-) create mode 100644 script/deploys/DeployEtherFiRestaker.s.sol create mode 100644 src/EtherFiRedemptionManager.sol rename test/{EtherFiWithdrawalBuffer.t.sol => EtherFiRedemptionManager.t.sol} (63%) diff --git a/script/deploys/DeployEtherFiRestaker.s.sol b/script/deploys/DeployEtherFiRestaker.s.sol new file mode 100644 index 000000000..0f4ec2225 --- /dev/null +++ b/script/deploys/DeployEtherFiRestaker.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; + +import "../../src/Liquifier.sol"; +import "../../src/EtherFiRestaker.sol"; +import "../../src/helpers/AddressProvider.sol"; +import "../../src/UUPSProxy.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +contract Deploy is Script { + using Strings for string; + + UUPSProxy public liquifierProxy; + + Liquifier public liquifierInstance; + + AddressProvider public addressProvider; + + address admin; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address addressProviderAddress = vm.envAddress("CONTRACT_REGISTRY"); + addressProvider = AddressProvider(addressProviderAddress); + + vm.startBroadcast(deployerPrivateKey); + + EtherFiRestaker restaker = EtherFiRestaker(payable(new UUPSProxy(payable(new EtherFiRestaker()), ""))); + restaker.initialize( + addressProvider.getContractAddress("LiquidityPool"), + addressProvider.getContractAddress("Liquifier") + ); + + new Liquifier(); + + // addressProvider.addContract(address(liquifierInstance), "Liquifier"); + + vm.stopBroadcast(); + } +} diff --git a/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol index 34f132e0c..ebc4050cf 100644 --- a/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol +++ b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol @@ -9,7 +9,7 @@ import "../../src/Liquifier.sol"; import "../../src/EtherFiRestaker.sol"; import "../../src/helpers/AddressProvider.sol"; import "../../src/UUPSProxy.sol"; -import "../../src/EtherFiWithdrawalBuffer.sol"; +import "../../src/EtherFiRedemptionManager.sol"; contract Deploy is Script { @@ -23,7 +23,7 @@ contract Deploy is Script { vm.startBroadcast(deployerPrivateKey); - EtherFiWithdrawalBuffer impl = new EtherFiWithdrawalBuffer( + EtherFiRedemptionManager impl = new EtherFiRedemptionManager( addressProvider.getContractAddress("LiquidityPool"), addressProvider.getContractAddress("EETH"), addressProvider.getContractAddress("WeETH"), @@ -32,7 +32,7 @@ contract Deploy is Script { ); UUPSProxy proxy = new UUPSProxy(payable(impl), ""); - EtherFiWithdrawalBuffer instance = EtherFiWithdrawalBuffer(payable(proxy)); + EtherFiRedemptionManager instance = EtherFiRedemptionManager(payable(proxy)); instance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); vm.stopBroadcast(); diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol new file mode 100644 index 000000000..c6c445c53 --- /dev/null +++ b/src/EtherFiRedemptionManager.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./interfaces/ILiquidityPool.sol"; +import "./interfaces/IeETH.sol"; +import "./interfaces/IWeETH.sol"; + +import "lib/BucketLimiter.sol"; + +import "./RoleRegistry.sol"; + +/* + The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. + - It has the exit fee as a percentage of the total amount redeemed. + - It has a rate limiter to limit the total amount that can be redeemed in a given time period. +*/ +contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 private constant BUCKET_UNIT_SCALE = 1e12; + uint256 private constant BASIS_POINT_SCALE = 1e4; + + bytes32 public constant PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); + bytes32 public constant PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); + bytes32 public constant PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); + + RoleRegistry public immutable roleRegistry; + address public immutable treasury; + IeETH public immutable eEth; + IWeETH public immutable weEth; + ILiquidityPool public immutable liquidityPool; + + BucketLimiter.Limit public limit; + uint16 public exitFeeSplitToTreasuryInBps; + uint16 public exitFeeInBps; + uint16 public lowWatermarkInBpsOfTvl; // bps of TVL + + event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); + + receive() external payable {} + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { + roleRegistry = RoleRegistry(_roleRegistry); + treasury = _treasury; + liquidityPool = ILiquidityPool(payable(_liquidityPool)); + eEth = IeETH(_eEth); + weEth = IWeETH(_weEth); + + _disableInitializers(); + } + + function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + exitFeeInBps = _exitFeeInBps; + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + /** + * @notice Redeems eETH for ETH. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the eETH. + */ + function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { + require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + _redeem(transferredEEthAmount, receiver); + } + + /** + * @notice Redeems weETH for ETH. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the weETH. + */ + function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { + uint256 eEthShares = weEthAmount; + uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); + require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); + weEth.unwrap(weEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + _redeem(transferredEEthAmount, receiver); + } + + + /** + * @notice Redeems ETH. + * @param ethAmount The amount of ETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + */ + function _redeem(uint256 ethAmount, address receiver) internal { + _updateRateLimit(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 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); + + uint256 ethShareFee = ethShares - sharesToBurn; + uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); + uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + + // Withdraw ETH from the liquidity pool + uint256 prevBalance = address(this).balance; + assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); + uint256 ethReceived = address(this).balance - prevBalance; + + // To Stakers by burning shares + eEth.burnShares(address(this), feeShareToStakers); + + // To Treasury by transferring eETH + IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + + // To Receiver by transferring ETH + (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); + require(success, "EtherFiRedemptionManager: Transfer failed"); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); + + emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); + } + + /** + * @dev if the contract has less than the low watermark, it will not allow any instant redemption. + */ + function lowWatermarkInETH() public view returns (uint256) { + return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + } + + /** + * @dev Returns the total amount that can be redeemed. + */ + function totalRedeemableAmount() external view returns (uint256) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { + return 0; + } + uint64 consumableBucketUnits = BucketLimiter.consumable(limit); + uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); + return Math.min(consumableAmount, liquidEthAmount); + } + + /** + * @dev Returns whether the given amount can be redeemed. + * @param amount The ETH or eETH amount to check. + */ + function canRedeem(uint256 amount) public view returns (bool) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { + return false; + } + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); + bool consumable = BucketLimiter.canConsume(limit, bucketUnit); + return consumable && amount <= liquidEthAmount; + } + + /** + * @dev Sets the maximum size of the bucket that can be consumed in a given time period. + * @param capacity The capacity of the bucket. + */ + function setCapacity(uint256 capacity) external hasRole(PROTOCOL_ADMIN) { + // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough + uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); + BucketLimiter.setCapacity(limit, bucketUnit); + } + + /** + * @dev Sets the rate at which the bucket is refilled per second. + * @param refillRate The rate at which the bucket is refilled per second. + */ + function setRefillRatePerSecond(uint256 refillRate) external hasRole(PROTOCOL_ADMIN) { + // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough + uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); + BucketLimiter.setRefillRate(limit, bucketUnit); + } + + /** + * @dev Sets the exit fee. + * @param _exitFeeInBps The exit fee. + */ + function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(PROTOCOL_ADMIN) { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeInBps = _exitFeeInBps; + } + + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(PROTOCOL_ADMIN) { + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(PROTOCOL_ADMIN) { + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + } + + function pauseContract() external hasRole(PROTOCOL_PAUSER) { + _pause(); + } + + function unPauseContract() external hasRole(PROTOCOL_UNPAUSER) { + _unpause(); + } + + function _updateRateLimit(uint256 amount) internal { + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); + require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); + } + + function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); + } + + function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { + return bucketUnit * BUCKET_UNIT_SCALE; + } + + /** + * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. + */ + // redeemable amount after exit fee + function previewRedeem(uint256 shares) public view returns (uint256) { + uint256 amountInEth = liquidityPool.amountForShare(shares); + return amountInEth - _fee(amountInEth, exitFeeInBps); + } + + 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 {} + + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + function _hasRole(bytes32 role, address account) internal view returns (bool) { + require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); + } + + modifier hasRole(bytes32 role) { + _hasRole(role, msg.sender); + _; + } + +} \ No newline at end of file diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index b51ed45b4..c6c445c53 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -27,7 +27,7 @@ import "./RoleRegistry.sol"; - It has the exit fee as a percentage of the total amount redeemed. - It has a rate limiter to limit the total amount that can be redeemed in a given time period. */ -contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { +contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { using SafeERC20 for IERC20; using Math for uint256; @@ -83,8 +83,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param owner The address of the owner of the eETH. */ function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); @@ -103,8 +103,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { uint256 eEthShares = weEthAmount; uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); - require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); @@ -149,8 +149,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); - require(success, "EtherFiWithdrawalBuffer: Transfer failed"); - require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Invalid liquidity pool balance"); + require(success, "EtherFiRedemptionManager: Transfer failed"); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } @@ -269,7 +269,7 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU } function _hasRole(bytes32 role, address account) internal view returns (bool) { - require(roleRegistry.hasRole(role, account), "EtherFiWithdrawalBuffer: Unauthorized"); + require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); } modifier hasRole(bytes32 role) { diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 28ee11fdd..910dd05fe 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -23,7 +23,7 @@ import "./interfaces/IEtherFiAdmin.sol"; import "./interfaces/IAuctionManager.sol"; import "./interfaces/ILiquifier.sol"; -import "./EtherFiWithdrawalBuffer.sol"; +import "./EtherFiRedemptionManager.sol"; contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, ILiquidityPool { @@ -72,7 +72,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL bool private isLpBnftHolder; - EtherFiWithdrawalBuffer public etherFiWithdrawalBuffer; + EtherFiRedemptionManager public etherFiRedemptionManager; //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- @@ -144,8 +144,9 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL liquifier = ILiquifier(_liquifier); } - function initializeOnUpgradeWithWithdrawalBuffer(address _withdrawalBuffer) external onlyOwner { - etherFiWithdrawalBuffer = EtherFiWithdrawalBuffer(payable(_withdrawalBuffer)); + function initializeOnUpgradeWithRedemptionManager(address _etherFiRedemptionManager) external onlyOwner { + require(address(etherFiRedemptionManager) == address(0), "Already initialized"); + etherFiRedemptionManager = EtherFiRedemptionManager(payable(_etherFiRedemptionManager)); } // Used by eETH staking flow @@ -188,7 +189,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL /// it returns the amount of shares burned function withdraw(address _recipient, uint256 _amount) external whenNotPaused returns (uint256) { uint256 share = sharesForWithdrawalAmount(_amount); - require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiWithdrawalBuffer), "Incorrect Caller"); + require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiRedemptionManager), "Incorrect Caller"); if (totalValueInLp < _amount || (msg.sender == address(withdrawRequestNFT) && ethAmountLockedForWithdrawal < _amount) || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); if (_amount > type(uint128).max || _amount == 0 || share == 0) revert InvalidAmount(); diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiRedemptionManager.t.sol similarity index 63% rename from test/EtherFiWithdrawalBuffer.t.sol rename to test/EtherFiRedemptionManager.t.sol index 7aede9fcf..f9f8da8e6 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/console2.sol"; import "./TestSetup.sol"; -contract EtherFiWithdrawalBufferTest is TestSetup { +contract EtherFiRedemptionManagerTest is TestSetup { address user = vm.addr(999); address op_admin = vm.addr(1000); @@ -20,14 +20,14 @@ contract EtherFiWithdrawalBufferTest is TestSetup { roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), op_admin); vm.stopPrank(); - etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); - etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark _upgrade_liquidity_pool_contract(); vm.prank(liquidityPoolInstance.owner()); - liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); + liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); } function test_rate_limit() public { @@ -35,33 +35,33 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.prank(user); liquidityPoolInstance.deposit{value: 1000 ether}(); - 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); + assertEq(etherFiRedemptionManagerInstance.canRedeem(1 ether), true); + assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether - 1), true); + assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether + 1), false); + assertEq(etherFiRedemptionManagerInstance.canRedeem(10 ether), false); + assertEq(etherFiRedemptionManagerInstance.totalRedeemableAmount(), 5 ether); } function test_lowwatermark_guardrail() public { vm.deal(user, 100 ether); - assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 0 ether); + assertEq(etherFiRedemptionManagerInstance.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); + vm.startPrank(etherFiRedemptionManagerInstance.owner()); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00); // 1% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 1 ether); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(50_00); // 50% - assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 50 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(50_00); // 50% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 50 ether); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_00); // 100% - assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 100 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_00); // 100% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 100 ether); vm.expectRevert("INVALID"); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% } function testFuzz_redeemEEth( @@ -84,22 +84,22 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Set exitFeeSplitToTreasuryInBps vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); // Set exitFeeBasisPoints and lowWatermarkInBpsOfTvl vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps); vm.prank(owner); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); vm.startPrank(user); - if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -118,7 +118,7 @@ contract EtherFiWithdrawalBufferTest is TestSetup { } else { vm.expectRevert(); - etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); } vm.stopPrank(); } @@ -151,13 +151,13 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Set fee and watermark configurations vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps); vm.prank(owner); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); // Convert redeemAmount from ETH to weETH vm.startPrank(user); @@ -165,14 +165,14 @@ contract EtherFiWithdrawalBufferTest is TestSetup { weEthInstance.wrap(redeemAmount); uint256 weEthAmount = weEthInstance.balanceOf(user); - if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), weEthAmount); - etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + weEthInstance.approve(address(etherFiRedemptionManagerInstance), weEthAmount); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -191,7 +191,7 @@ contract EtherFiWithdrawalBufferTest is TestSetup { } else { vm.expectRevert(); - etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); } vm.stopPrank(); } @@ -217,23 +217,23 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Admin performs admin-only actions vm.startPrank(admin); - etherFiWithdrawalBufferInstance.setCapacity(10 ether); - etherFiWithdrawalBufferInstance.setRefillRatePerSecond(0.001 ether); - etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(1e4); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(1e2); - etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(1e2); + etherFiRedemptionManagerInstance.setCapacity(10 ether); + etherFiRedemptionManagerInstance.setRefillRatePerSecond(0.001 ether); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(1e4); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1e2); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(1e2); vm.stopPrank(); // Pauser pauses the contract vm.startPrank(pauser); - etherFiWithdrawalBufferInstance.pauseContract(); - assertTrue(etherFiWithdrawalBufferInstance.paused()); + etherFiRedemptionManagerInstance.pauseContract(); + assertTrue(etherFiRedemptionManagerInstance.paused()); vm.stopPrank(); // Unpauser unpauses the contract vm.startPrank(unpauser); - etherFiWithdrawalBufferInstance.unPauseContract(); - assertFalse(etherFiWithdrawalBufferInstance.paused()); + etherFiRedemptionManagerInstance.unPauseContract(); + assertFalse(etherFiRedemptionManagerInstance.paused()); vm.stopPrank(); // Revoke PROTOCOL_ADMIN role from admin @@ -242,28 +242,28 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Admin attempts admin-only actions after role revocation vm.startPrank(admin); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.setCapacity(10 ether); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.setCapacity(10 ether); vm.stopPrank(); // Pauser attempts to unpause (should fail) vm.startPrank(pauser); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.unPauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.unPauseContract(); vm.stopPrank(); // Unpauser attempts to pause (should fail) vm.startPrank(unpauser); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.pauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.pauseContract(); vm.stopPrank(); // User without role attempts admin-only actions vm.startPrank(user); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.pauseContract(); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.unPauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.pauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.unPauseContract(); vm.stopPrank(); } @@ -280,23 +280,23 @@ contract EtherFiWithdrawalBufferTest is TestSetup { liquidityPoolInstance.deposit{value: 10 ether}(); - uint256 redeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + uint256 redeemableAmount = etherFiRedemptionManagerInstance.totalRedeemableAmount(); uint256 userBalance = address(user).balance; - uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); - etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, user); uint256 totalFee = (1 ether * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; uint256 userReceives = 1 ether - totalFee; - assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())), treasuryBalance + treasuryFee, 1e1); assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 5 ether); + vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); + etherFiRedemptionManagerInstance.redeemEEth(5 ether, user, user); vm.stopPrank(); } @@ -326,8 +326,8 @@ contract EtherFiWithdrawalBufferTest is TestSetup { uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); - etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + weEthInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); uint256 totalFee = (eEthAmount * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -347,8 +347,8 @@ contract EtherFiWithdrawalBufferTest is TestSetup { eETHInstance.mintShares(user, 2 * redeemAmount); vm.startPrank(op_admin); - etherFiWithdrawalBufferInstance.setCapacity(2 * redeemAmount); - etherFiWithdrawalBufferInstance.setRefillRatePerSecond(2 * redeemAmount); + etherFiRedemptionManagerInstance.setCapacity(2 * redeemAmount); + etherFiRedemptionManagerInstance.setRefillRatePerSecond(2 * redeemAmount); vm.stopPrank(); vm.warp(block.timestamp + 1); @@ -356,11 +356,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.startPrank(user); uint256 userBalance = address(user).balance; - uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); + vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); vm.stopPrank(); } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index af6a4ca82..f27c906b0 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -49,7 +49,7 @@ import "../src/EtherFiAdmin.sol"; import "../src/EtherFiTimelock.sol"; import "../src/BucketRateLimiter.sol"; -import "../src/EtherFiWithdrawalBuffer.sol"; +import "../src/EtherFiRedemptionManager.sol"; contract TestSetup is Test { @@ -103,7 +103,7 @@ contract TestSetup is Test { UUPSProxy public membershipNftProxy; UUPSProxy public nftExchangeProxy; UUPSProxy public withdrawRequestNFTProxy; - UUPSProxy public etherFiWithdrawalBufferProxy; + UUPSProxy public etherFiRedemptionManagerProxy; UUPSProxy public etherFiOracleProxy; UUPSProxy public etherFiAdminProxy; UUPSProxy public roleRegistryProxy; @@ -164,7 +164,7 @@ contract TestSetup is Test { WithdrawRequestNFT public withdrawRequestNFTImplementation; WithdrawRequestNFT public withdrawRequestNFTInstance; - EtherFiWithdrawalBuffer public etherFiWithdrawalBufferInstance; + EtherFiRedemptionManager public etherFiRedemptionManagerInstance; NFTExchange public nftExchangeImplementation; NFTExchange public nftExchangeInstance; @@ -584,14 +584,14 @@ contract TestSetup is Test { roleRegistry = RoleRegistry(address(roleRegistryProxy)); roleRegistry.initialize(owner); - etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); - etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), owner); liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); - liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); + liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); membershipNftInstance.initialize("https://etherfi-cdn/{id}.json", address(membershipManagerInstance)); withdrawRequestNFTInstance.initialize(payable(address(liquidityPoolInstance)), payable(address(eETHInstance)), payable(address(membershipManagerInstance))); From bcc1184daa0f7c28a90d56e5fdd0aea43b474ccc Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 00:29:44 +0900 Subject: [PATCH 14/95] fix the logic to check the aggr calls --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 90b939b72..6a47c3c3b 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -233,7 +233,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); - require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "Not all requests have been scanned"); + require(_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); uint256 beforeEEthShares = eETH.shares(address(this)); From 1f85fb6911e403879a714ca77c8843a5b43d70a5 Mon Sep 17 00:00:00 2001 From: Jacob T Firek <106350168+jtfirek@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:14:49 -0500 Subject: [PATCH 15/95] Update test/WithdrawRequestNFT.t.sol Signed-off-by: Jacob T Firek <106350168+jtfirek@users.noreply.github.com> --- test/WithdrawRequestNFT.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 7e3e62944..172362a37 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -357,7 +357,7 @@ contract WithdrawRequestNFTTest is TestSetup { test_aggregateSumEEthShareAmount(); vm.startPrank(withdrawRequestNFTInstance.owner()); - vm.expectRevert("Not all requests have been scanned"); + vm.expectRevert("Not all prev requests have been scanned"); withdrawRequestNFTInstance.handleRemainder(1 ether); vm.stopPrank(); From bdee463a963731570728e7df1a0955bd12c6c7ec Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 07:48:47 +0900 Subject: [PATCH 16/95] reduce gas spending for 'call', update the upgrade init function, remove unused file --- src/EtherFiRedemptionManager.sol | 2 +- src/EtherFiWithdrawalBuffer.sol | 280 ------------------------------- src/WithdrawRequestNFT.sol | 4 +- test/WithdrawRequestNFT.t.sol | 3 +- 4 files changed, 5 insertions(+), 284 deletions(-) delete mode 100644 src/EtherFiWithdrawalBuffer.sol diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index c6c445c53..434e46f25 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -148,7 +148,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); // To Receiver by transferring ETH - (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); + (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol deleted file mode 100644 index c6c445c53..000000000 --- a/src/EtherFiWithdrawalBuffer.sol +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; -import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; - -import "./interfaces/ILiquidityPool.sol"; -import "./interfaces/IeETH.sol"; -import "./interfaces/IWeETH.sol"; - -import "lib/BucketLimiter.sol"; - -import "./RoleRegistry.sol"; - -/* - The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. - - It has the exit fee as a percentage of the total amount redeemed. - - It has a rate limiter to limit the total amount that can be redeemed in a given time period. -*/ -contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { - using SafeERC20 for IERC20; - using Math for uint256; - - uint256 private constant BUCKET_UNIT_SCALE = 1e12; - uint256 private constant BASIS_POINT_SCALE = 1e4; - - bytes32 public constant PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); - bytes32 public constant PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); - bytes32 public constant PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); - - RoleRegistry public immutable roleRegistry; - address public immutable treasury; - IeETH public immutable eEth; - IWeETH public immutable weEth; - ILiquidityPool public immutable liquidityPool; - - BucketLimiter.Limit public limit; - uint16 public exitFeeSplitToTreasuryInBps; - uint16 public exitFeeInBps; - uint16 public lowWatermarkInBpsOfTvl; // bps of TVL - - event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); - - receive() external payable {} - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { - roleRegistry = RoleRegistry(_roleRegistry); - treasury = _treasury; - liquidityPool = ILiquidityPool(payable(_liquidityPool)); - eEth = IeETH(_eEth); - weEth = IWeETH(_weEth); - - _disableInitializers(); - } - - function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { - __Ownable_init(); - __UUPSUpgradeable_init(); - __Pausable_init(); - __ReentrancyGuard_init(); - - limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); - exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; - exitFeeInBps = _exitFeeInBps; - lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; - } - - /** - * @notice Redeems eETH for ETH. - * @param eEthAmount The amount of eETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the eETH. - */ - function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); - IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); - } - - /** - * @notice Redeems weETH for ETH. - * @param weEthAmount The amount of weETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the weETH. - */ - function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - uint256 eEthShares = weEthAmount; - uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); - require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); - IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); - weEth.unwrap(weEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); - } - - - /** - * @notice Redeems ETH. - * @param ethAmount The amount of ETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - */ - function _redeem(uint256 ethAmount, address receiver) internal { - _updateRateLimit(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 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); - - uint256 ethShareFee = ethShares - sharesToBurn; - uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); - uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; - - // Withdraw ETH from the liquidity pool - uint256 prevBalance = address(this).balance; - assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); - uint256 ethReceived = address(this).balance - prevBalance; - - // To Stakers by burning shares - eEth.burnShares(address(this), feeShareToStakers); - - // To Treasury by transferring eETH - IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - - // To Receiver by transferring ETH - (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); - require(success, "EtherFiRedemptionManager: Transfer failed"); - require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); - - emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); - } - - /** - * @dev if the contract has less than the low watermark, it will not allow any instant redemption. - */ - function lowWatermarkInETH() public view returns (uint256) { - return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); - } - - /** - * @dev Returns the total amount that can be redeemed. - */ - function totalRedeemableAmount() external view returns (uint256) { - uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); - if (liquidEthAmount < lowWatermarkInETH()) { - return 0; - } - uint64 consumableBucketUnits = BucketLimiter.consumable(limit); - uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); - return Math.min(consumableAmount, liquidEthAmount); - } - - /** - * @dev Returns whether the given amount can be redeemed. - * @param amount The ETH or eETH amount to check. - */ - function canRedeem(uint256 amount) public view returns (bool) { - uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); - if (liquidEthAmount < lowWatermarkInETH()) { - return false; - } - uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); - bool consumable = BucketLimiter.canConsume(limit, bucketUnit); - return consumable && amount <= liquidEthAmount; - } - - /** - * @dev Sets the maximum size of the bucket that can be consumed in a given time period. - * @param capacity The capacity of the bucket. - */ - function setCapacity(uint256 capacity) external hasRole(PROTOCOL_ADMIN) { - // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough - uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); - BucketLimiter.setCapacity(limit, bucketUnit); - } - - /** - * @dev Sets the rate at which the bucket is refilled per second. - * @param refillRate The rate at which the bucket is refilled per second. - */ - function setRefillRatePerSecond(uint256 refillRate) external hasRole(PROTOCOL_ADMIN) { - // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough - uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); - BucketLimiter.setRefillRate(limit, bucketUnit); - } - - /** - * @dev Sets the exit fee. - * @param _exitFeeInBps The exit fee. - */ - function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(PROTOCOL_ADMIN) { - require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); - exitFeeInBps = _exitFeeInBps; - } - - function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(PROTOCOL_ADMIN) { - require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); - lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; - } - - function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(PROTOCOL_ADMIN) { - require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); - exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; - } - - function pauseContract() external hasRole(PROTOCOL_PAUSER) { - _pause(); - } - - function unPauseContract() external hasRole(PROTOCOL_UNPAUSER) { - _unpause(); - } - - function _updateRateLimit(uint256 amount) internal { - uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); - require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); - } - - function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { - return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); - } - - function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { - return bucketUnit * BUCKET_UNIT_SCALE; - } - - /** - * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. - */ - // redeemable amount after exit fee - function previewRedeem(uint256 shares) public view returns (uint256) { - uint256 amountInEth = liquidityPool.amountForShare(shares); - return amountInEth - _fee(amountInEth, exitFeeInBps); - } - - 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 {} - - function getImplementation() external view returns (address) { - return _getImplementation(); - } - - function _hasRole(bytes32 role, address account) internal view returns (bool) { - require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); - } - - modifier hasRole(bytes32 role) { - _hasRole(role, msg.sender); - _; - } - -} \ No newline at end of file diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 6a47c3c3b..2ad797057 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -70,12 +70,14 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad nextRequestId = 1; } - function initializeOnUpgrade(address _pauser) external onlyOwner { + function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { require(pauser == address(0), "Already initialized"); paused = false; pauser = _pauser; + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; + _currentRequestIdToScanFromForShareRemainder = 1; _lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; } diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 172362a37..1c8b326c6 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -327,8 +327,7 @@ contract WithdrawRequestNFTTest is TestSetup { vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); - withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet); - withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); + withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet, 50_00); withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); // 2. PAUSE From d40a117159983f852ffd40978876048315b1121d Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 08:10:15 +0900 Subject: [PATCH 17/95] apply gas opt for BucketLimiter --- lib/BucketLimiter.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/BucketLimiter.sol b/lib/BucketLimiter.sol index 83f749156..36dc9a6ed 100644 --- a/lib/BucketLimiter.sol +++ b/lib/BucketLimiter.sol @@ -111,6 +111,11 @@ library BucketLimiter { function _refill(Limit memory limit) internal view { // We allow for overflow here, as the delta is resilient against it. uint64 now_ = uint64(block.timestamp); + + if (now_ == limit.lastRefill) { + return; + } + uint64 delta; unchecked { delta = now_ - limit.lastRefill; From f4f2ffdf7340128abcb1e2e43e3a9558d9de2456 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 08:32:15 +0900 Subject: [PATCH 18/95] improve assetion tsets, apply design pattern, function rename --- src/EtherFiRedemptionManager.sol | 7 +++++-- src/WithdrawRequestNFT.sol | 11 ++++++----- test/WithdrawRequestNFT.t.sol | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 434e46f25..a44a2eaa3 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -125,8 +125,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _updateRateLimit(ethAmount); uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); - uint256 ethShareToReceiver = ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE); - uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShareToReceiver); + uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver uint256 prevLpBalance = address(liquidityPool).balance; uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); @@ -137,6 +136,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; // Withdraw ETH from the liquidity pool + uint256 totalEEthShare = eEth.totalShares(); uint256 prevBalance = address(this).balance; assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); uint256 ethReceived = address(this).balance - prevBalance; @@ -150,7 +150,10 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); + + // Make sure the liquidity pool balance is correct && total shares are correct require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); + require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 2ad797057..27207a294 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -130,12 +130,13 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad require(request.isValid, "Request is not valid"); uint256 amountToWithdraw = getClaimableAmount(tokenId); + uint256 shareAmountToBurnForWithdrawal = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); // transfer eth to recipient _burn(tokenId); delete _requests[tokenId]; - - uint256 shareAmountToBurnForWithdrawal = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); + + // update accounting totalLockedEEthShares -= shareAmountToBurnForWithdrawal; uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); @@ -166,7 +167,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } // Seize the request simply by transferring it to another recipient - function seizeRequest(uint256 requestId, address recipient) external onlyOwner { + function seizeInvalidRequest(uint256 requestId, address recipient) external onlyOwner { require(!_requests[requestId].isValid, "Request is valid"); require(_exists(requestId), "Request does not exist"); @@ -184,7 +185,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function isValid(uint256 requestId) public view returns (bool) { - require(_exists(requestId), "Request does not exist11"); + require(_exists(requestId), "Request does not exist"); return _requests[requestId].isValid; } @@ -200,7 +201,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function validateRequest(uint256 requestId) external onlyAdmin { - require(_exists(requestId), "Request does not exist22"); + require(_exists(requestId), "Request does not exist"); require(!_requests[requestId].isValid, "Request is valid"); _requests[requestId].isValid = true; diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 1c8b326c6..a13d3f406 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -556,6 +556,6 @@ contract WithdrawRequestNFTTest is TestSetup { // Owner can seize the invalidated request NFT vm.prank(withdrawRequestNFTInstance.owner()); - withdrawRequestNFTInstance.seizeRequest(requestId, admin); + withdrawRequestNFTInstance.seizeInvalidRequest(requestId, admin); } } From 5069c14ed7a59cae801e7121a89d364945b3ff67 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 08:39:26 +0900 Subject: [PATCH 19/95] apply CEI pattern to 'handleRemainder' --- src/WithdrawRequestNFT.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 27207a294..0ac30749b 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -242,15 +242,16 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(_eEthAmount); uint256 eEthSharesToTreasury = eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); - eETH.transfer(treasury, eEthAmountToTreasury); - uint256 eEthSharesToBurn = eEthShares - eEthSharesToTreasury; + uint256 eEthSharesToMoved = eEthSharesToTreasury + eEthSharesToBurn; + + totalLockedEEthShares -= eEthSharesToMoved; + + eETH.transfer(treasury, eEthAmountToTreasury); eETH.burnShares(address(this), eEthSharesToBurn); - uint256 reducedEEthShares = beforeEEthShares - eETH.shares(address(this)); - totalLockedEEthShares -= reducedEEthShares; + require (beforeEEthShares - eEthSharesToMoved == eETH.shares(address(this)), "Invalid eETH shares after remainder handling"); emit HandledRemainderOfClaimedWithdrawRequests(eEthAmountToTreasury, liquidityPool.amountForShare(eEthSharesToBurn)); } From 2e132028be4e8aa3b31fe1c12b277457f1c02b79 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 10:04:58 +0900 Subject: [PATCH 20/95] apply CEI pattern to 'redeem' --- src/EtherFiRedemptionManager.sol | 46 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index a44a2eaa3..6497c15a2 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -86,12 +86,11 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } /** @@ -101,18 +100,16 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param owner The address of the owner of the weETH. */ function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - uint256 eEthShares = weEthAmount; - uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); + uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); weEth.unwrap(weEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } @@ -121,23 +118,19 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param ethAmount The amount of ETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. */ - function _redeem(uint256 ethAmount, address receiver) internal { + function _redeem(uint256 ethAmount, uint256 eEthShares, address receiver, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) internal { _updateRateLimit(ethAmount); - uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); - uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver + // Derive additionals + uint256 eEthShareFee = eEthShares - sharesToBurn; + uint256 feeShareToStakers = eEthShareFee - feeShareToTreasury; + // Snapshot balances & shares for sanity check at the end + uint256 prevBalance = address(this).balance; uint256 prevLpBalance = address(liquidityPool).balance; - uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); - - uint256 ethShareFee = ethShares - sharesToBurn; - uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); - uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + uint256 totalEEthShare = eEth.totalShares(); // Withdraw ETH from the liquidity pool - uint256 totalEEthShare = eEth.totalShares(); - uint256 prevBalance = address(this).balance; assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); uint256 ethReceived = address(this).balance - prevBalance; @@ -252,6 +245,17 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable return bucketUnit * BUCKET_UNIT_SCALE; } + + function _calcRedemption(uint256 ethAmount) internal view returns (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) { + eEthShares = liquidityPool.sharesForAmount(ethAmount); + eEthAmountToReceiver = liquidityPool.amountForShare(eEthShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver + + sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); + uint256 eEthShareFee = eEthShares - sharesToBurn; + feeShareToTreasury = eEthShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); + } + /** * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. */ From b18bd18b77dbab102d6d49ad6d92c3a6121c2a64 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:29:04 +0900 Subject: [PATCH 21/95] use 'totalRemainderEEthShares' instead of locked share --- src/WithdrawRequestNFT.sol | 33 +++++++++++++++++++-------------- test/WithdrawRequestNFT.t.sol | 18 +++++++----------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 0ac30749b..540d2151e 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -33,8 +33,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // inclusive uint32 private _currentRequestIdToScanFromForShareRemainder; uint32 private _lastRequestIdToScanUntilForShareRemainder; + uint256 public _aggregateSumOfEEthShare; - uint256 public totalLockedEEthShares; + uint256 public totalRemainderEEthShares; bool public paused; address public pauser; @@ -94,7 +95,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 feeGwei = uint32(fee / 1 gwei); _requests[requestId] = IWithdrawRequestNFT.WithdrawRequest(amountOfEEth, shareOfEEth, true, feeGwei); - totalLockedEEthShares += shareOfEEth; _safeMint(recipient, requestId); @@ -137,7 +137,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad delete _requests[tokenId]; // update accounting - totalLockedEEthShares -= shareAmountToBurnForWithdrawal; + totalRemainderEEthShares += request.shareOfEEth - shareAmountToBurnForWithdrawal; uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); assert (amountBurnedShare == shareAmountToBurnForWithdrawal); @@ -160,10 +160,17 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad for (uint256 i = scanFrom; i <= scanUntil; i++) { if (!_exists(i)) continue; - totalLockedEEthShares += _requests[i].shareOfEEth; + _aggregateSumOfEEthShare += _requests[i].shareOfEEth; } _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); + + // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` + if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { + require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "new req has been created"); + totalRemainderEEthShares = eETH.shares(address(this)) - _aggregateSumOfEEthShare; + _aggregateSumOfEEthShare = 0; // gone + } } // Seize the request simply by transferring it to another recipient @@ -235,18 +242,17 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); require(_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); + require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); uint256 beforeEEthShares = eETH.shares(address(this)); - - uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(_eEthAmount); - uint256 eEthSharesToTreasury = eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); - uint256 eEthSharesToBurn = eEthShares - eEthSharesToTreasury; - uint256 eEthSharesToMoved = eEthSharesToTreasury + eEthSharesToBurn; - totalLockedEEthShares -= eEthSharesToMoved; + uint256 eEthAmountToTreasury = _eEthAmount.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); + uint256 eEthAmountToBurn = _eEthAmount - eEthAmountToTreasury; + uint256 eEthSharesToBurn = liquidityPool.sharesForAmount(eEthAmountToBurn); + uint256 eEthSharesToMoved = eEthSharesToBurn + liquidityPool.sharesForAmount(eEthAmountToTreasury); + + totalRemainderEEthShares -= eEthSharesToMoved; eETH.transfer(treasury, eEthAmountToTreasury); eETH.burnShares(address(this), eEthSharesToBurn); @@ -257,8 +263,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function getEEthRemainderAmount() public view returns (uint256) { - uint256 eEthRemainderShare = eETH.shares(address(this)) - totalLockedEEthShares; - return liquidityPool.amountForShare(eEthRemainderShare); + return liquidityPool.amountForShare(totalRemainderEEthShares); } // the withdraw request NFT is transferrable diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index a13d3f406..da8874b71 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -417,6 +417,8 @@ contract WithdrawRequestNFTTest is TestSetup { vm.assume(remainderSplitBps <= 10000); vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance)); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(10); + // Setup initial balance for recipient vm.deal(recipient, depositAmount); @@ -431,7 +433,6 @@ contract WithdrawRequestNFTTest is TestSetup { // Record initial balances uint256 treasuryEEthBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 recipientBalanceBefore = address(recipient).balance; - uint256 initialTotalLockedEEthShares = withdrawRequestNFTInstance.totalLockedEEthShares(); // Request withdraw eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); @@ -441,8 +442,6 @@ contract WithdrawRequestNFTTest is TestSetup { // Get initial request state WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); - assertEq(withdrawRequestNFTInstance.totalLockedEEthShares(), initialTotalLockedEEthShares + request.shareOfEEth, "Incorrect total locked shares"); - // Simulate rebase after request but before claim vm.prank(address(membershipManagerInstance)); liquidityPoolInstance.rebase(int128(uint128(rebaseAmount))); @@ -497,20 +496,17 @@ contract WithdrawRequestNFTTest is TestSetup { vm.expectRevert("ERC721: invalid token ID"); withdrawRequestNFTInstance.ownerOf(requestId); - // Calculate and verify remainder splitting - assertApproxEqAbs( - expectedLockedShares, - withdrawRequestNFTInstance.totalLockedEEthShares(), - 1e1, - "Incorrect locked eETH share" - ); - // Verify recipient received correct ETH amount assertEq( address(recipient).balance, recipientBalanceBefore + expectedWithdrawAmount, "Recipient should receive correct ETH amount" ); + + uint256 expectedLockedEEthAmount = liquidityPoolInstance.amountForShare(expectedLockedShares); + withdrawRequestNFTInstance.getEEthRemainderAmount(); + vm.prank(admin); + withdrawRequestNFTInstance.handleRemainder(expectedLockedEEthAmount); } function testFuzz_InvalidateRequest(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { From 1e126738b703d96535abe67da7c221ca6c85757e Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:30:36 +0900 Subject: [PATCH 22/95] initializeOnUpgrade cant be called twice --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 540d2151e..3ee26dce1 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -72,7 +72,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { - require(pauser == address(0), "Already initialized"); + require(_currentRequestIdToScanFromForShareRemainder == 0, "Already initialized"); paused = false; pauser = _pauser; From c14c348fb7d5e271b3c5aad5b9a01fc10ff6ef55 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:44:45 +0900 Subject: [PATCH 23/95] initializeOnUpgrade onlyOnce --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 3ee26dce1..f97f65c13 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -72,7 +72,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { - require(_currentRequestIdToScanFromForShareRemainder == 0, "Already initialized"); + require(pauser == address(0) && _pauser != address(0), "Already initialized"); paused = false; pauser = _pauser; From 35fba66bdd28ea08a351c44963b41818a960edb6 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:48:15 +0900 Subject: [PATCH 24/95] use uint256 instead of uint32 --- src/WithdrawRequestNFT.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index f97f65c13..3f46010bb 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -31,8 +31,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint16 public shareRemainderSplitToTreasuryInBps; // inclusive - uint32 private _currentRequestIdToScanFromForShareRemainder; - uint32 private _lastRequestIdToScanUntilForShareRemainder; + uint256 private _currentRequestIdToScanFromForShareRemainder; + uint256 private _lastRequestIdToScanUntilForShareRemainder; uint256 public _aggregateSumOfEEthShare; uint256 public totalRemainderEEthShares; @@ -163,7 +163,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad _aggregateSumOfEEthShare += _requests[i].shareOfEEth; } - _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); + _currentRequestIdToScanFromForShareRemainder = scanUntil + 1; // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { From e95cfc023c6589c205a3ca489b75f285eb610a8d Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:50:14 +0900 Subject: [PATCH 25/95] revert --- src/WithdrawRequestNFT.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 3f46010bb..f97f65c13 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -31,8 +31,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint16 public shareRemainderSplitToTreasuryInBps; // inclusive - uint256 private _currentRequestIdToScanFromForShareRemainder; - uint256 private _lastRequestIdToScanUntilForShareRemainder; + uint32 private _currentRequestIdToScanFromForShareRemainder; + uint32 private _lastRequestIdToScanUntilForShareRemainder; uint256 public _aggregateSumOfEEthShare; uint256 public totalRemainderEEthShares; @@ -163,7 +163,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad _aggregateSumOfEEthShare += _requests[i].shareOfEEth; } - _currentRequestIdToScanFromForShareRemainder = scanUntil + 1; + _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { From bbf2d83a568832f97b332220f898a4899ba28400 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 08:10:18 +0900 Subject: [PATCH 26/95] improve the fuzz test --- test/WithdrawRequestNFT.t.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index da8874b71..5d354b7da 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -449,8 +449,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Calculate expected withdrawal amounts after rebase uint256 sharesValue = liquidityPoolInstance.amountForShare(request.shareOfEEth); uint256 expectedWithdrawAmount = withdrawAmount < sharesValue ? withdrawAmount : sharesValue; - uint256 expectedBurnedShares = liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); - uint256 expectedLockedShares = request.shareOfEEth - expectedBurnedShares; + uint256 expectedBurnedShares = liquidityPoolInstance.sharesForAmount(expectedWithdrawAmount); + uint256 expectedDustShares = request.shareOfEEth - expectedBurnedShares; // Track initial shares and total supply uint256 initialTotalShares = eETHInstance.totalShares(); @@ -503,10 +503,17 @@ contract WithdrawRequestNFTTest is TestSetup { "Recipient should receive correct ETH amount" ); - uint256 expectedLockedEEthAmount = liquidityPoolInstance.amountForShare(expectedLockedShares); - withdrawRequestNFTInstance.getEEthRemainderAmount(); - vm.prank(admin); - withdrawRequestNFTInstance.handleRemainder(expectedLockedEEthAmount); + assertApproxEqAbs( + withdrawRequestNFTInstance.totalRemainderEEthShares(), + expectedDustShares, + 1, + "Incorrect remainder shares" + ); + + uint256 dustEEthAmount = withdrawRequestNFTInstance.getEEthRemainderAmount(); + vm.startPrank(admin); + withdrawRequestNFTInstance.handleRemainder(dustEEthAmount / 2); + withdrawRequestNFTInstance.handleRemainder(dustEEthAmount / 2); } function testFuzz_InvalidateRequest(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { From 2cbbc047354a0280d0719a1d76b82bf60a52f967 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 19:51:10 +0900 Subject: [PATCH 27/95] (1) pause the contract on upgrade, (2) prevent from calling 'aggregateSumEEthShareAmount' again after the scan is completed, (3) prevent from undoing the finalization 'finalizeRequests' --- src/WithdrawRequestNFT.sol | 7 +++++-- test/WithdrawRequestNFT.t.sol | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index f97f65c13..759183ac4 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -73,8 +73,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { require(pauser == address(0) && _pauser != address(0), "Already initialized"); + require(_shareRemainderSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); - paused = false; + paused = true; // make sure the contract is paused after the upgrade pauser = _pauser; shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; @@ -154,6 +155,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // This function is used to aggregate the sum of the eEth shares of the requests that have not been claimed yet. // To be triggered during the upgrade to the new version of the contract. function aggregateSumEEthShareAmount(uint256 _numReqsToScan) external { + require(_currentRequestIdToScanFromForShareRemainder != _lastRequestIdToScanUntilForShareRemainder + 1, "scan is completed"); + // [scanFrom, scanUntil] uint256 scanFrom = _currentRequestIdToScanFromForShareRemainder; uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); @@ -167,7 +170,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { - require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "new req has been created"); totalRemainderEEthShares = eETH.shares(address(this)) - _aggregateSumOfEEthShare; _aggregateSumOfEEthShare = 0; // gone } @@ -197,6 +199,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function finalizeRequests(uint256 requestId) external onlyAdmin { + require(requestId > lastFinalizedRequestId, "Cannot undo finalization"); lastFinalizedRequestId = uint32(requestId); } diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 5d354b7da..21f5f31d0 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -419,6 +419,9 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.aggregateSumEEthShareAmount(10); + vm.expectRevert("scan is completed"); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(10); + // Setup initial balance for recipient vm.deal(recipient, depositAmount); From 1482cb0a599661a6d7de68f861db801c0ec54799 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 09:29:30 +0900 Subject: [PATCH 28/95] only owner of the funds can call {redeemEEth, redeemWeEth} --- src/EtherFiRedemptionManager.sol | 14 ++++++-------- test/EtherFiRedemptionManager.t.sol | 16 ++++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 6497c15a2..c5449096b 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -80,15 +80,14 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @notice Redeems eETH for ETH. * @param eEthAmount The amount of eETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the eETH. */ - function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + function redeemEEth(uint256 eEthAmount, address receiver) public whenNotPaused nonReentrant { + require(eEthAmount <= eEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); + IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), eEthAmount); _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } @@ -97,16 +96,15 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @notice Redeems weETH for ETH. * @param weEthAmount The amount of weETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the weETH. */ - function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { + function redeemWeEth(uint256 weEthAmount, address receiver) public whenNotPaused nonReentrant { uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); - require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(weEthAmount <= weEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); + IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), weEthAmount); weEth.unwrap(weEthAmount); _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index f9f8da8e6..6979fdf9c 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -99,7 +99,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -118,7 +118,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { } else { vm.expectRevert(); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); } vm.stopPrank(); } @@ -172,7 +172,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); weEthInstance.approve(address(etherFiRedemptionManagerInstance), weEthAmount); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -191,7 +191,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { } else { vm.expectRevert(); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); } vm.stopPrank(); } @@ -285,7 +285,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); - etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, user); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user); uint256 totalFee = (1 ether * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -296,7 +296,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { eETHInstance.approve(address(etherFiRedemptionManagerInstance), 5 ether); vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); - etherFiRedemptionManagerInstance.redeemEEth(5 ether, user, user); + etherFiRedemptionManagerInstance.redeemEEth(5 ether, user); vm.stopPrank(); } @@ -327,7 +327,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); weEthInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); uint256 totalFee = (eEthAmount * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -360,7 +360,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); vm.stopPrank(); } From 8ced81ecd1d4ff1a0ace4ac3cf18c7a882f5398d Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:13:32 +0900 Subject: [PATCH 29/95] disable unpause until the scan is completed --- src/WithdrawRequestNFT.sol | 36 ++++++++++-------- test/EtherFiRedemptionManager.t.sol | 12 ++++++ test/WithdrawRequestNFT.t.sol | 59 +++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 759183ac4..fd7fbb6a2 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -31,9 +31,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint16 public shareRemainderSplitToTreasuryInBps; // inclusive - uint32 private _currentRequestIdToScanFromForShareRemainder; - uint32 private _lastRequestIdToScanUntilForShareRemainder; - uint256 public _aggregateSumOfEEthShare; + uint32 public currentRequestIdToScanFromForShareRemainder; + uint32 public lastRequestIdToScanUntilForShareRemainder; + uint256 public aggregateSumOfEEthShare; uint256 public totalRemainderEEthShares; @@ -80,8 +80,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; - _currentRequestIdToScanFromForShareRemainder = 1; - _lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; + currentRequestIdToScanFromForShareRemainder = 1; + lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; } /// @notice creates a withdraw request and issues an associated NFT to the recipient @@ -155,23 +155,23 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // This function is used to aggregate the sum of the eEth shares of the requests that have not been claimed yet. // To be triggered during the upgrade to the new version of the contract. function aggregateSumEEthShareAmount(uint256 _numReqsToScan) external { - require(_currentRequestIdToScanFromForShareRemainder != _lastRequestIdToScanUntilForShareRemainder + 1, "scan is completed"); + require(!isScanOfShareRemainderCompleted(), "scan is completed"); // [scanFrom, scanUntil] - uint256 scanFrom = _currentRequestIdToScanFromForShareRemainder; - uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); + uint256 scanFrom = currentRequestIdToScanFromForShareRemainder; + uint256 scanUntil = Math.min(lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); for (uint256 i = scanFrom; i <= scanUntil; i++) { if (!_exists(i)) continue; - _aggregateSumOfEEthShare += _requests[i].shareOfEEth; + aggregateSumOfEEthShare += _requests[i].shareOfEEth; } - _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); + currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); - // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` - if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { - totalRemainderEEthShares = eETH.shares(address(this)) - _aggregateSumOfEEthShare; - _aggregateSumOfEEthShare = 0; // gone + // When the scan is completed, update the `totalRemainderEEthShares` and reset the `aggregateSumOfEEthShare` + if (isScanOfShareRemainderCompleted()) { + totalRemainderEEthShares = eETH.shares(address(this)) - aggregateSumOfEEthShare; + aggregateSumOfEEthShare = 0; // gone } } @@ -234,6 +234,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function unPauseContract() external onlyAdmin { + require(isScanOfShareRemainderCompleted(), "scan is not completed"); + paused = false; emit Unpaused(msg.sender); } @@ -245,7 +247,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require(_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); + require(currentRequestIdToScanFromForShareRemainder == lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); uint256 beforeEEthShares = eETH.shares(address(this)); @@ -269,6 +271,10 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad return liquidityPool.amountForShare(totalRemainderEEthShares); } + function isScanOfShareRemainderCompleted() public view returns (bool) { + return currentRequestIdToScanFromForShareRemainder == (lastRequestIdToScanUntilForShareRemainder + 1); + } + // the withdraw request NFT is transferrable // - if the request is valid, it can be transferred by the owner of the NFT // - if the request is invalid, it can be transferred only by the owner of the WithdarwRequestNFT contract diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index 6979fdf9c..cd3f3cc10 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -30,6 +30,18 @@ contract EtherFiRedemptionManagerTest is TestSetup { liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); } + function test_upgrade_only_by_owner() public { + setUp_Fork(); + + address impl = etherFiRedemptionManagerInstance.getImplementation(); + vm.prank(admin); + vm.expectRevert(); + etherFiRedemptionManagerInstance.upgradeTo(impl); + + vm.prank(etherFiRedemptionManagerInstance.owner()); + etherFiRedemptionManagerInstance.upgradeTo(impl); + } + function test_rate_limit() public { vm.deal(user, 1000 ether); vm.prank(user); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 21f5f31d0..465dc4215 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -6,14 +6,35 @@ pragma solidity ^0.8.13; import "forge-std/console2.sol"; import "./TestSetup.sol"; + +contract WithdrawRequestNFTIntrusive is WithdrawRequestNFT { + + constructor() WithdrawRequestNFT(address(0)) {} + + function updateParam(uint32 _currentRequestIdToScanFromForShareRemainder, uint32 _lastRequestIdToScanUntilForShareRemainder) external { + currentRequestIdToScanFromForShareRemainder = _currentRequestIdToScanFromForShareRemainder; + lastRequestIdToScanUntilForShareRemainder = _lastRequestIdToScanUntilForShareRemainder; + } + +} + contract WithdrawRequestNFTTest is TestSetup { uint32[] public reqIds =[ 20, 388, 478, 714, 726, 729, 735, 815, 861, 916, 941, 1014, 1067, 1154, 1194, 1253]; + address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; function setUp() public { setUpTests(); } + function updateParam(uint32 _currentRequestIdToScanFromForShareRemainder, uint32 _lastRequestIdToScanUntilForShareRemainder) internal { + address cur_impl = withdrawRequestNFTInstance.getImplementation(); + address new_impl = address(new WithdrawRequestNFTIntrusive()); + withdrawRequestNFTInstance.upgradeTo(new_impl); + WithdrawRequestNFTIntrusive(address(withdrawRequestNFTInstance)).updateParam(_currentRequestIdToScanFromForShareRemainder, _lastRequestIdToScanUntilForShareRemainder); + withdrawRequestNFTInstance.upgradeTo(cur_impl); + } + function test_finalizeRequests() public { startHoax(bob); liquidityPoolInstance.deposit{value: 10 ether}(); @@ -322,42 +343,50 @@ contract WithdrawRequestNFTTest is TestSetup { function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); - address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; - vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet, 50_00); withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); - // 2. PAUSE - withdrawRequestNFTInstance.pauseContract(); - vm.stopPrank(); + // 2. (For test) update the scan range + updateParam(1, 200); - vm.startPrank(etherfi_admin_wallet); + // 2. Confirm Paused & Can't Unpause + assertTrue(withdrawRequestNFTInstance.paused(), "Contract should be paused"); + vm.expectRevert("scan is not completed"); + withdrawRequestNFTInstance.unPauseContract(); + vm.stopPrank(); // 3. AggSum + // - Can't Unpause untill the scan is not completed + // - Can't aggregateSumEEthShareAmount after the scan is completed withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); - // ... + assertFalse(withdrawRequestNFTInstance.isScanOfShareRemainderCompleted(), "Scan should be completed"); - vm.stopPrank(); + vm.prank(withdrawRequestNFTInstance.owner()); + vm.expectRevert("scan is not completed"); + withdrawRequestNFTInstance.unPauseContract(); + + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); + assertTrue(withdrawRequestNFTInstance.isScanOfShareRemainderCompleted(), "Scan should be completed"); - // 4. Unpause + vm.expectRevert("scan is completed"); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); + + // 4. Can Unpause vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.unPauseContract(); vm.stopPrank(); - // Back to normal - vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[1])); - withdrawRequestNFTInstance.claimWithdraw(reqIds[1]); + // we will run the test on the forked mainnet to perform the full scan and confirm we can unpause } function test_handleRemainder() public { test_aggregateSumEEthShareAmount(); - vm.startPrank(withdrawRequestNFTInstance.owner()); - vm.expectRevert("Not all prev requests have been scanned"); - withdrawRequestNFTInstance.handleRemainder(1 ether); + vm.prank(etherfi_admin_wallet); + withdrawRequestNFTInstance.handleRemainder(0.01 ether); vm.stopPrank(); } From 86ac4a0d8d2dc34f5f3b79cb9c285177262133f8 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:13:57 +0900 Subject: [PATCH 30/95] check the basis points params are below 1e4 --- src/EtherFiRedemptionManager.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index c5449096b..404f66e74 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -65,6 +65,10 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable } function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + __Ownable_init(); __UUPSUpgradeable_init(); __Pausable_init(); From 1e8cdeab1cc35a9f3a60ada3dea4f33d692214c8 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:16:39 +0900 Subject: [PATCH 31/95] disallow calling 'initializeOnUpgradeWithRedemptionManager' with invalid param & calling it twice --- src/LiquidityPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 910dd05fe..622f80441 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -145,7 +145,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function initializeOnUpgradeWithRedemptionManager(address _etherFiRedemptionManager) external onlyOwner { - require(address(etherFiRedemptionManager) == address(0), "Already initialized"); + require(address(etherFiRedemptionManager) == address(0) && _etherFiRedemptionManager != address(0), "Invalid"); etherFiRedemptionManager = EtherFiRedemptionManager(payable(_etherFiRedemptionManager)); } From 6fffba6d6ba161a7129e136cf58152036ad5e656 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:23:15 +0900 Subject: [PATCH 32/95] (1) withdrawRequestNFT cannot call LiquidityPool.addEthAmountLockedForWithdrawal, (2) remove unused function reduceEthAmountLockedForWithdrawal --- src/LiquidityPool.sol | 8 +------- src/interfaces/ILiquidityPool.sol | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 622f80441..6fc71c1bc 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -532,17 +532,11 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function addEthAmountLockedForWithdrawal(uint128 _amount) external { - if (!(msg.sender == address(etherFiAdminContract) || msg.sender == address(withdrawRequestNFT))) revert IncorrectCaller(); + if (!(msg.sender == address(etherFiAdminContract))) revert IncorrectCaller(); ethAmountLockedForWithdrawal += _amount; } - function reduceEthAmountLockedForWithdrawal(uint128 _amount) external { - if (msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); - - ethAmountLockedForWithdrawal -= _amount; - } - //-------------------------------------------------------------------------------------- //------------------------------ INTERNAL FUNCTIONS ---------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index a71b2d6c7..9400955db 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -71,7 +71,6 @@ interface ILiquidityPool { function rebase(int128 _accruedRewards) external; function payProtocolFees(uint128 _protocolFees) external; function addEthAmountLockedForWithdrawal(uint128 _amount) external; - function reduceEthAmountLockedForWithdrawal(uint128 _amount) external; function setStakingTargetWeights(uint32 _eEthWeight, uint32 _etherFanWeight) external; function updateAdmin(address _newAdmin, bool _isAdmin) external; From 89db9d5739a12e343e57a10e61f63c3a596b1a25 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 13:59:25 +0900 Subject: [PATCH 33/95] prevent finalizing future requests --- script/specialized/weEth_withdrawal_v2.s.sol | 69 ++++++++++++++++++++ src/WithdrawRequestNFT.sol | 1 + test/WithdrawRequestNFT.t.sol | 4 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 script/specialized/weEth_withdrawal_v2.s.sol diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol new file mode 100644 index 000000000..680028cde --- /dev/null +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "test/TestSetup.sol"; + +import "src/helpers/AddressProvider.sol"; + +contract Upgrade is Script { + + AddressProvider public addressProvider; + address public addressProviderAddress = 0x8487c5F8550E3C3e7734Fe7DCF77DB2B72E4A848; + address public roleRegistry = 0x1d3Af47C1607A2EF33033693A9989D1d1013BB50; + address public treasury = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + address public pauser = 0x9AF1298993DC1f397973C62A5D47a284CF76844D; + + WithdrawRequestNFT withdrawRequestNFTInstance; + LiquidityPool liquidityPoolInstance; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + AddressProvider addressProvider = AddressProvider(addressProviderAddress); + + withdrawRequestNFTInstance = WithdrawRequestNFT(payable(addressProvider.getContractAddress("WithdrawRequestNFT"))); + liquidityPoolInstance = LiquidityPool(payable(addressProvider.getContractAddress("LiquidityPool"))); + + vm.startBroadcast(deployerPrivateKey); + + // deploy_upgrade(); + // agg(); + // handle_remainder(); + + vm.stopBroadcast(); + } + + function deploy_upgrade() internal { + UUPSProxy etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager( + addressProvider.getContractAddress("LiquidityPool"), + addressProvider.getContractAddress("EETH"), + addressProvider.getContractAddress("WeETH"), + treasury, + roleRegistry)), ""); + EtherFiRedemptionManager etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + + withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(treasury))); + withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); // 50% fee split to treasury + + liquidityPoolInstance.upgradeTo(address(new LiquidityPool())); + liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); + } + + function agg() internal { + uint256 numToScanPerTx = 1024; + uint256 cnt = (withdrawRequestNFTInstance.nextRequestId() / numToScanPerTx) + 1; + console.log(cnt); + for (uint256 i = 0; i < cnt; i++) { + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(numToScanPerTx); + } + } + + function handle_remainder() internal { + withdrawRequestNFTInstance.updateAdmin(msg.sender, true); + withdrawRequestNFTInstance.unPauseContract(); + uint256 remainder = withdrawRequestNFTInstance.getEEthRemainderAmount(); + console.log(remainder); + withdrawRequestNFTInstance.handleRemainder(remainder); + } +} \ No newline at end of file diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index fd7fbb6a2..cd03bcc1e 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -200,6 +200,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad function finalizeRequests(uint256 requestId) external onlyAdmin { require(requestId > lastFinalizedRequestId, "Cannot undo finalization"); + require(requestId < nextRequestId, "Cannot finalize future requests"); lastFinalizedRequestId = uint32(requestId); } diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 465dc4215..02954b842 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -343,10 +343,12 @@ contract WithdrawRequestNFTTest is TestSetup { function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); + address pauser = 0x9af1298993dc1f397973c62a5d47a284cf76844d; + vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); - withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet, 50_00); + withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); // 2. (For test) update the scan range From c5a2dd1a39cff016106f8cf5481576009d3e4992 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 14:19:23 +0900 Subject: [PATCH 34/95] add {redeemEEthWithPermit, redeemWeEthWithPermit}, use try-catch for --- src/EtherFiRedemptionManager.sol | 9 +++++++++ src/LiquidityPool.sol | 2 +- src/interfaces/IWeETH.sol | 9 +++++++++ src/interfaces/IeETH.sol | 9 +++++++++ test/EtherFiRedemptionManager.t.sol | 10 +++++----- test/TestSetup.sol | 28 ++++++++++++++++++++++++++++ test/WithdrawRequestNFT.t.sol | 2 +- 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 404f66e74..5fc5fd8ed 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -114,6 +114,15 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } + function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external { + try eEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + redeemEEth(eEthAmount, receiver); + } + + function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external { + try weEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + redeemWeEth(weEthAmount, receiver); + } /** * @notice Redeems ETH. diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 6fc71c1bc..8a95400fa 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -235,7 +235,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL whenNotPaused returns (uint256) { - eETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s); + try eETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s) {} catch {} return requestWithdraw(_owner, _amount); } diff --git a/src/interfaces/IWeETH.sol b/src/interfaces/IWeETH.sol index b64d76ca3..4741da079 100644 --- a/src/interfaces/IWeETH.sol +++ b/src/interfaces/IWeETH.sol @@ -6,6 +6,15 @@ import "./ILiquidityPool.sol"; import "./IeETH.sol"; interface IWeETH is IERC20Upgradeable { + + struct PermitInput { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + // STATE VARIABLES function eETH() external view returns (IeETH); function liquidityPool() external view returns (ILiquidityPool); diff --git a/src/interfaces/IeETH.sol b/src/interfaces/IeETH.sol index 74fc6affa..f8ee974b9 100644 --- a/src/interfaces/IeETH.sol +++ b/src/interfaces/IeETH.sol @@ -2,6 +2,15 @@ pragma solidity ^0.8.13; interface IeETH { + + struct PermitInput { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + function name() external pure returns (string memory); function symbol() external pure returns (string memory); function decimals() external pure returns (uint8); diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index cd3f3cc10..7aa5374d4 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -109,9 +109,9 @@ contract EtherFiRedemptionManagerTest is TestSetup { if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); - - eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); + + IeETH.PermitInput memory permit = eEth_createPermitInput(999, address(etherFiRedemptionManagerInstance), redeemAmount, eETHInstance.nonces(user), 2**256 - 1, eETHInstance.DOMAIN_SEPARATOR()); + etherFiRedemptionManagerInstance.redeemEEthWithPermit(redeemAmount, user, permit); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -183,8 +183,8 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); - weEthInstance.approve(address(etherFiRedemptionManagerInstance), weEthAmount); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); + IWeETH.PermitInput memory permit = weEth_createPermitInput(999, address(etherFiRedemptionManagerInstance), weEthAmount, weEthInstance.nonces(user), 2**256 - 1, weEthInstance.DOMAIN_SEPARATOR()); + etherFiRedemptionManagerInstance.redeemWeEthWithPermit(weEthAmount, user, permit); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; diff --git a/test/TestSetup.sol b/test/TestSetup.sol index f27c906b0..4eca2441f 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -1033,6 +1033,34 @@ contract TestSetup is Test { return permitInput; } + function eEth_createPermitInput(uint256 privKey, address spender, uint256 value, uint256 nonce, uint256 deadline, bytes32 domianSeparator) public returns (IeETH.PermitInput memory) { + address _owner = vm.addr(privKey); + bytes32 digest = calculatePermitDigest(_owner, spender, value, nonce, deadline, domianSeparator); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); + IeETH.PermitInput memory permitInput = IeETH.PermitInput({ + value: value, + deadline: deadline, + v: v, + r: r, + s: s + }); + return permitInput; + } + + function weEth_createPermitInput(uint256 privKey, address spender, uint256 value, uint256 nonce, uint256 deadline, bytes32 domianSeparator) public returns (IWeETH.PermitInput memory) { + address _owner = vm.addr(privKey); + bytes32 digest = calculatePermitDigest(_owner, spender, value, nonce, deadline, domianSeparator); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); + IWeETH.PermitInput memory permitInput = IWeETH.PermitInput({ + value: value, + deadline: deadline, + v: v, + r: r, + s: s + }); + return permitInput; + } + function registerAsBnftHolder(address _user) internal { (bool registered, uint32 index) = liquidityPoolInstance.bnftHoldersIndexes(_user); if (!registered) liquidityPoolInstance.registerAsBnftHolder(_user); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 02954b842..aeebdcb35 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -343,7 +343,7 @@ contract WithdrawRequestNFTTest is TestSetup { function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); - address pauser = 0x9af1298993dc1f397973c62a5d47a284cf76844d; + address pauser = 0x9AF1298993DC1f397973C62A5D47a284CF76844D; vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade From c85e92028d4eda92273ef38fa40840c1c5b21ff9 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 15:28:16 +0900 Subject: [PATCH 35/95] remove a redundant check --- src/WithdrawRequestNFT.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index cd03bcc1e..96a5213ad 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -104,7 +104,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function getClaimableAmount(uint256 tokenId) public view returns (uint256) { - require(tokenId < nextRequestId, "Request does not exist"); require(tokenId <= lastFinalizedRequestId, "Request is not finalized"); require(ownerOf(tokenId) != address(0), "Already Claimed"); From cca361ec30d5fbf76cfdc31aac2993b94ece0c75 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 18:10:42 +0900 Subject: [PATCH 36/95] Prevent 'initializeOnUpgrade' of LiquidityPool from being called twice, add the unused gap var and add initialization for added variables --- src/LiquidityPool.sol | 2 +- src/WithdrawRequestNFT.sol | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 8a95400fa..13f2d6e0f 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -138,7 +138,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function initializeOnUpgrade(address _auctionManager, address _liquifier) external onlyOwner { - require(_auctionManager != address(0) && _liquifier != address(0), "Invalid params"); + require(_auctionManager != address(0) && _liquifier != address(0) && address(auctionManager) == address(0) && address(liquifier) == address(0), "Invalid"); auctionManager = IAuctionManager(_auctionManager); liquifier = ILiquifier(_liquifier); diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 96a5213ad..0c3088cd5 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -29,6 +29,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 public nextRequestId; uint32 public lastFinalizedRequestId; uint16 public shareRemainderSplitToTreasuryInBps; + uint16 public _unused_gap; // inclusive uint32 public currentRequestIdToScanFromForShareRemainder; @@ -78,10 +79,15 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad paused = true; // make sure the contract is paused after the upgrade pauser = _pauser; + _unused_gap = 0; + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; currentRequestIdToScanFromForShareRemainder = 1; lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; + + aggregateSumOfEEthShare = 0; + totalRemainderEEthShares = 0; } /// @notice creates a withdraw request and issues an associated NFT to the recipient From 268efe5b9085d281a5dd2676b3fb4ccd3055af74 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 18:13:46 +0900 Subject: [PATCH 37/95] use 'isScanOfShareRemainderCompleted' --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 0c3088cd5..a5fbeb413 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -253,7 +253,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require(currentRequestIdToScanFromForShareRemainder == lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); + require(isScanOfShareRemainderCompleted(), "Not all prev requests have been scanned"); require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); uint256 beforeEEthShares = eETH.shares(address(this)); From a13919db2ef14ce3ec2c869264f42bb4f2d7300c Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 20:40:37 +0900 Subject: [PATCH 38/95] add the max value constraint on rate limit --- src/EtherFiRedemptionManager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 5fc5fd8ed..9e9b97cd4 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -249,6 +249,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable } function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + require(amount <= type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); } From 58fe3fbcfc345f797be222619a1efe0b4a287bce Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 20:42:21 +0900 Subject: [PATCH 39/95] remove equality condition --- src/EtherFiRedemptionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 9e9b97cd4..630f959c4 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -249,7 +249,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable } function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { - require(amount <= type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); + require(amount < type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); } From 898896a34b893a1809a49620f610ab68798407f2 Mon Sep 17 00:00:00 2001 From: syko Date: Thu, 2 Jan 2025 23:41:16 +0900 Subject: [PATCH 40/95] fix the overflow issue in '_refill' Signed-off-by: syko --- lib/BucketLimiter.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/BucketLimiter.sol b/lib/BucketLimiter.sol index 36dc9a6ed..87a70a2f1 100644 --- a/lib/BucketLimiter.sol +++ b/lib/BucketLimiter.sol @@ -109,23 +109,22 @@ library BucketLimiter { } function _refill(Limit memory limit) internal view { - // We allow for overflow here, as the delta is resilient against it. uint64 now_ = uint64(block.timestamp); if (now_ == limit.lastRefill) { return; } - uint64 delta; + uint256 delta; unchecked { delta = now_ - limit.lastRefill; } - uint64 tokens = delta * limit.refillRate; - uint64 newRemaining = limit.remaining + tokens; + uint256 tokens = delta * uint256(limit.refillRate); + uint256 newRemaining = uint256(limit.remaining) + tokens; if (newRemaining > limit.capacity) { limit.remaining = limit.capacity; } else { - limit.remaining = newRemaining; + limit.remaining = uint64(newRemaining); } limit.lastRefill = now_; } @@ -167,4 +166,4 @@ library BucketLimiter { refill(limit); limit.remaining = remaining; } -} \ No newline at end of file +} From 25ac914b3b48ec57cf232e82424b3e0903a1b011 Mon Sep 17 00:00:00 2001 From: syko Date: Thu, 2 Jan 2025 23:49:18 +0900 Subject: [PATCH 41/95] Update EtherFiRedemptionManager.sol Signed-off-by: syko --- src/EtherFiRedemptionManager.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 630f959c4..3969e624c 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -151,6 +151,8 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Treasury by transferring eETH IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); @@ -296,4 +298,4 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _; } -} \ No newline at end of file +} From fd52a523caf8f34a1e79a6d9ca7eb95203b490cc Mon Sep 17 00:00:00 2001 From: syko Date: Fri, 3 Jan 2025 00:08:28 +0900 Subject: [PATCH 42/95] prevent the total shares from going below 1 gwei after redemption Signed-off-by: syko --- src/EtherFiRedemptionManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 3969e624c..5ff6b5cf7 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -151,7 +151,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Treasury by transferring eETH IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); @@ -159,7 +159,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // Make sure the liquidity pool balance is correct && total shares are correct require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); - require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } From 6eefec09f6793cd31f9440aaf65964ef042a50c6 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 6 Dec 2024 13:03:10 +0900 Subject: [PATCH 43/95] init. Instant Withdrawal via Buffer --- lib/BucketLimiter.sol | 5 + src/EtherFiWithdrawalBuffer.sol | 224 ++++++++++++++++++++++++++++++++ src/LiquidityPool.sol | 11 +- 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/EtherFiWithdrawalBuffer.sol diff --git a/lib/BucketLimiter.sol b/lib/BucketLimiter.sol index eb410f78e..83f749156 100644 --- a/lib/BucketLimiter.sol +++ b/lib/BucketLimiter.sol @@ -71,6 +71,11 @@ library BucketLimiter { return limit.remaining >= amount; } + function consumable(Limit memory limit) external view returns (uint64) { + _refill(limit); + return limit.remaining; + } + /* * Consumes the given amount from the bucket, if there is sufficient capacity, and returns * whether the bucket had enough remaining capacity to consume the amount. diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol new file mode 100644 index 000000000..95bf930ce --- /dev/null +++ b/src/EtherFiWithdrawalBuffer.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./interfaces/ILiquidityPool.sol"; +import "./interfaces/IeETH.sol"; +import "./interfaces/IWeETH.sol"; + +import "lib/BucketLimiter.sol"; + + +/* + The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. + - It has the exit fee as a percentage of the total amount redeemed. + - It has a rate limiter to limit the total amount that can be redeemed in a given time period. +*/ +contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 private constant BUCKEt_UNIT_SCALE = 1e12; + uint256 private constant BASIS_POINT_SCALE = 1e4; + address public immutable feeReceiver; + 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 + + 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"); + + feeReceiver = _feeReceiver; + liquidityPool = ILiquidityPool(payable(_liquidityPool)); + eEth = IeETH(_eEth); + weEth = IWeETH(_weEth); + + _disableInitializers(); + } + + function initialize(uint256 _exitFeeBasisPoints, uint256 _lowWatermarkInBpsOfTvl) external initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + limit = BucketLimiter.create(0, 0); + exitFeeBasisPoints = _exitFeeBasisPoints; + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + /** + * @notice Redeems eETH for ETH. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the eETH. + * @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"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + return _redeem(transferredEEthAmount, receiver); + } + + /** + * @notice Redeems weETH for ETH. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the weETH. + * @return The amount of ETH sent to the receiver and the exit fee amount. + */ + function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { + uint256 eEthShares = weEthAmount; + uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); + require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); + weEth.unwrap(weEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + return _redeem(transferredEEthAmount, receiver); + } + + + /** + * @notice Redeems ETH. + * @param ethAmount The amount of ETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @return The amount of ETH sent to the receiver and the exit fee amount. + */ + 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 prevBalance = address(this).balance; + payable(feeReceiver).transfer(ethFeeAmount); + payable(receiver).transfer(ethToReceiver); + uint256 ethSent = address(this).balance - prevBalance; + require(ethSent == ethAmount, "EtherFiWithdrawalBuffer: Transfer failed"); + + return (ethToReceiver, ethFeeAmount); + } + + /** + * @dev if the contract has less than the low watermark, it will not allow any instant redemption. + */ + function lowWatermarkInETH() public view returns (uint256) { + return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + } + + /** + * @dev Returns the total amount that can be redeemed. + */ + function totalRedeemableAmount() external view returns (uint256) { + uint64 consumableBucketUnits = BucketLimiter.consumable(limit); + uint256 consumableAmount = _convertBucketUnitToAmount(consumableBucketUnits); + return consumableAmount; + } + + /** + * @dev Returns whether the given amount can be redeemed. + * @param amount The ETH or eETH amount to check. + */ + function canRedeem(uint256 amount) public view returns (bool) { + if (address(liquidityPool).balance < lowWatermarkInETH()) { + return false; + } + uint64 bucketUnit = _convertSharesToBucketUnit(amount, Math.Rounding.Up); + bool consumable = BucketLimiter.canConsume(limit, bucketUnit); + return consumable; + } + + /** + * @dev Sets the maximum size of the bucket that can be consumed in a given time period. + * @param capacity The capacity of the bucket. + */ + function setCapacity(uint256 capacity) external onlyOwner { + // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough + uint64 bucketUnit = _convertSharesToBucketUnit(capacity, Math.Rounding.Down); + BucketLimiter.setCapacity(limit, bucketUnit); + } + + /** + * @dev Sets the rate at which the bucket is refilled per second. + * @param refillRate The rate at which the bucket is refilled per second. + */ + function setRefillRatePerSecond(uint256 refillRate) external onlyOwner { + // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough + uint64 bucketUnit = _convertSharesToBucketUnit(refillRate, Math.Rounding.Down); + BucketLimiter.setRefillRate(limit, bucketUnit); + } + + /** + * @dev Sets the exit fee. + * @param _exitFeeBasisPoints The exit fee. + */ + function setExitFeeBasisPoints(uint256 _exitFeeBasisPoints) external onlyOwner { + exitFeeBasisPoints = _exitFeeBasisPoints; + } + + function _updateRateLimit(uint256 shares) internal { + uint64 bucketUnit = _convertSharesToBucketUnit(shares, Math.Rounding.Up); + require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); + } + + 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); + } + + function _convertBucketUnitToAmount(uint64 bucketUnit) internal pure returns (uint256) { + return bucketUnit * BUCKEt_UNIT_SCALE; + } + + /** + * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. + */ + // redeemable amount after exit fee + function previewRedeem(uint256 shares) public view returns (uint256) { + uint256 amountInEth = liquidityPool.amountForShare(shares); + return amountInEth - _feeOnTotal(amountInEth, exitFeeBasisPoints); + } + + /** + * @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 _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + +} \ No newline at end of file diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 93e032abf..28ee11fdd 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -23,6 +23,9 @@ import "./interfaces/IEtherFiAdmin.sol"; import "./interfaces/IAuctionManager.sol"; import "./interfaces/ILiquifier.sol"; +import "./EtherFiWithdrawalBuffer.sol"; + + contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, ILiquidityPool { //-------------------------------------------------------------------------------------- //--------------------------------- STATE-VARIABLES ---------------------------------- @@ -69,6 +72,8 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL bool private isLpBnftHolder; + EtherFiWithdrawalBuffer public etherFiWithdrawalBuffer; + //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- //-------------------------------------------------------------------------------------- @@ -139,6 +144,10 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL liquifier = ILiquifier(_liquifier); } + function initializeOnUpgradeWithWithdrawalBuffer(address _withdrawalBuffer) external onlyOwner { + etherFiWithdrawalBuffer = EtherFiWithdrawalBuffer(payable(_withdrawalBuffer)); + } + // Used by eETH staking flow function deposit() external payable returns (uint256) { return deposit(address(0)); @@ -179,7 +188,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL /// it returns the amount of shares burned function withdraw(address _recipient, uint256 _amount) external whenNotPaused returns (uint256) { uint256 share = sharesForWithdrawalAmount(_amount); - require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager), "Incorrect Caller"); + require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiWithdrawalBuffer), "Incorrect Caller"); if (totalValueInLp < _amount || (msg.sender == address(withdrawRequestNFT) && ethAmountLockedForWithdrawal < _amount) || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); if (_amount > type(uint128).max || _amount == 0 || share == 0) revert InvalidAmount(); From 9963471ee75a7d909a4c59cf2256c9a003109e59 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 9 Dec 2024 17:36:21 +0900 Subject: [PATCH 44/95] 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 95bf930ce..811d8d149 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 64d30ae4f..439f0fa24 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 000000000..ce7f99548 --- /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 8af079431..af3eff876 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"; import "../script/ContractCodeChecker.sol"; import "../script/Create2Factory.sol"; @@ -107,6 +108,7 @@ contract TestSetup is Test, ContractCodeChecker { UUPSProxy public membershipNftProxy; UUPSProxy public nftExchangeProxy; UUPSProxy public withdrawRequestNFTProxy; + UUPSProxy public etherFiWithdrawalBufferProxy; UUPSProxy public etherFiOracleProxy; UUPSProxy public etherFiAdminProxy; @@ -166,6 +168,8 @@ contract TestSetup is Test, ContractCodeChecker { WithdrawRequestNFT public withdrawRequestNFTImplementation; WithdrawRequestNFT public withdrawRequestNFTInstance; + EtherFiWithdrawalBuffer public etherFiWithdrawalBufferInstance; + NFTExchange public nftExchangeImplementation; NFTExchange public nftExchangeInstance; @@ -556,7 +560,7 @@ contract TestSetup is Test, ContractCodeChecker { 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)); @@ -577,7 +581,13 @@ contract TestSetup is Test, ContractCodeChecker { 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 98accc85b..924545421 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); + } } From 9dcb732db834518a5f5164285dc8588ecabce1db Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 12 Dec 2024 09:33:34 +0900 Subject: [PATCH 45/95] fix scripts --- script/deploys/DeployPhaseTwo.s.sol | 2 +- script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/deploys/DeployPhaseTwo.s.sol b/script/deploys/DeployPhaseTwo.s.sol index a144e0a35..2417e191b 100644 --- a/script/deploys/DeployPhaseTwo.s.sol +++ b/script/deploys/DeployPhaseTwo.s.sol @@ -81,7 +81,7 @@ contract DeployPhaseTwoScript is Script { } retrieve_contract_addresses(); - withdrawRequestNftImplementation = new WithdrawRequestNFT(); + withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0), 0); withdrawRequestNftProxy = new UUPSProxy(address(withdrawRequestNftImplementation), ""); withdrawRequestNftInstance = WithdrawRequestNFT(payable(withdrawRequestNftProxy)); diff --git a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol index 823cf90a5..5862fb36e 100644 --- a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol +++ b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol @@ -20,7 +20,7 @@ contract WithdrawRequestNFTUpgrade is Script { vm.startBroadcast(deployerPrivateKey); WithdrawRequestNFT oracleInstance = WithdrawRequestNFT(proxyAddress); - WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(); + WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(address(0), 0); oracleInstance.upgradeTo(address(v2Implementation)); From 0e35c6b25b838d4385b0872774d602c4b13f2d1a Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 17 Dec 2024 14:43:25 +0900 Subject: [PATCH 46/95] add role registry, consider eth amount locked for withdrawal in liquidity pool, add tests --- src/EtherFiWithdrawalBuffer.sol | 85 ++++++--- src/interfaces/ILiquidityPool.sol | 1 + test/EtherFiWithdrawalBuffer.t.sol | 271 ++++++++++++++++++++++++++++- test/TestSetup.sol | 14 +- 4 files changed, 336 insertions(+), 35 deletions(-) diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index 811d8d149..a79b9e948 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -20,6 +20,7 @@ import "./interfaces/IWeETH.sol"; import "lib/BucketLimiter.sol"; +import "./RoleRegistry.sol"; /* The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. @@ -32,6 +33,12 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 private constant BUCKET_UNIT_SCALE = 1e12; uint256 private constant BASIS_POINT_SCALE = 1e4; + + bytes32 public constant PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); + bytes32 public constant PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); + bytes32 public constant PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); + + RoleRegistry public immutable roleRegistry; address public immutable treasury; IeETH public immutable eEth; IWeETH public immutable weEth; @@ -45,9 +52,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor - 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"); - + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { + roleRegistry = RoleRegistry(_roleRegistry); treasury = _treasury; liquidityPool = ILiquidityPool(payable(_liquidityPool)); eEth = IeETH(_eEth); @@ -56,13 +62,13 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU _disableInitializers(); } - function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl) external initializer { + function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { __Ownable_init(); __UUPSUpgradeable_init(); __Pausable_init(); __ReentrancyGuard_init(); - limit = BucketLimiter.create(0, 0); + limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; exitFeeInBps = _exitFeeInBps; lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; @@ -76,8 +82,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) { - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); @@ -97,8 +103,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { uint256 eEthShares = weEthAmount; uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); @@ -135,14 +141,14 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; // To Stakers by burning shares - eEth.burnShares(address(this), liquidityPool.sharesForAmount(feeShareToStakers)); + eEth.burnShares(address(this), 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"); + (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); + require(success && address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Transfer failed"); return (ethReceived, eEthAmountFee); } @@ -158,12 +164,13 @@ 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()) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { return 0; } uint64 consumableBucketUnits = BucketLimiter.consumable(limit); - uint256 consumableAmount = _convertBucketUnitToAmount(consumableBucketUnits); - return consumableAmount; + uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); + return Math.min(consumableAmount, liquidEthAmount); } /** @@ -171,21 +178,22 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param amount The ETH or eETH amount to check. */ function canRedeem(uint256 amount) public view returns (bool) { - if (address(liquidityPool).balance < lowWatermarkInETH()) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { return false; } - uint64 bucketUnit = _convertSharesToBucketUnit(amount, Math.Rounding.Up); + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); bool consumable = BucketLimiter.canConsume(limit, bucketUnit); - return consumable; + return consumable && amount <= liquidEthAmount; } /** * @dev Sets the maximum size of the bucket that can be consumed in a given time period. * @param capacity The capacity of the bucket. */ - function setCapacity(uint256 capacity) external onlyOwner { + function setCapacity(uint256 capacity) external hasRole(PROTOCOL_ADMIN) { // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough - uint64 bucketUnit = _convertSharesToBucketUnit(capacity, Math.Rounding.Down); + uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); BucketLimiter.setCapacity(limit, bucketUnit); } @@ -193,9 +201,9 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @dev Sets the rate at which the bucket is refilled per second. * @param refillRate The rate at which the bucket is refilled per second. */ - function setRefillRatePerSecond(uint256 refillRate) external onlyOwner { + function setRefillRatePerSecond(uint256 refillRate) external hasRole(PROTOCOL_ADMIN) { // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough - uint64 bucketUnit = _convertSharesToBucketUnit(refillRate, Math.Rounding.Down); + uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); BucketLimiter.setRefillRate(limit, bucketUnit); } @@ -203,31 +211,39 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @dev Sets the exit fee. * @param _exitFeeInBps The exit fee. */ - function setExitFeeBasisPoints(uint16 _exitFeeInBps) external onlyOwner { + function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(PROTOCOL_ADMIN) { require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); exitFeeInBps = _exitFeeInBps; } - function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external onlyOwner { + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(PROTOCOL_ADMIN) { require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; } - function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external onlyOwner { + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(PROTOCOL_ADMIN) { require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; } - function _updateRateLimit(uint256 shares) internal { - uint64 bucketUnit = _convertSharesToBucketUnit(shares, Math.Rounding.Up); + function pauseContract() external hasRole(PROTOCOL_PAUSER) { + _pause(); + } + + function unPauseContract() external hasRole(PROTOCOL_UNPAUSER) { + _unpause(); + } + + function _updateRateLimit(uint256 amount) internal { + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); } - 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); + function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); } - function _convertBucketUnitToAmount(uint64 bucketUnit) internal pure returns (uint256) { + function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { return bucketUnit * BUCKET_UNIT_SCALE; } @@ -246,4 +262,17 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + function _hasRole(bytes32 role, address account) internal view returns (bool) { + require(roleRegistry.hasRole(role, account), "EtherFiWithdrawalBuffer: Unauthorized"); + } + + modifier hasRole(bytes32 role) { + _hasRole(role, msg.sender); + _; + } + } \ No newline at end of file diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 2f12fd76d..a71b2d6c7 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -50,6 +50,7 @@ interface ILiquidityPool { function sharesForAmount(uint256 _amount) external view returns (uint256); function sharesForWithdrawalAmount(uint256 _amount) external view returns (uint256); function amountForShare(uint256 _share) external view returns (uint256); + function ethAmountLockedForWithdrawal() external view returns (uint128); function deposit() external payable returns (uint256); function deposit(address _referral) external payable returns (uint256); diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol index ce7f99548..d73268fd0 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -7,6 +7,7 @@ import "./TestSetup.sol"; contract EtherFiWithdrawalBufferTest is TestSetup { address user = vm.addr(999); + address op_admin = vm.addr(1000); function setUp() public { setUpTests(); @@ -18,8 +19,18 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.stopPrank(); vm.warp(block.timestamp + 5 * 1000); // 0.001 ether * 5000 = 5 ether refilled + } + + function setUp_Fork() public { + initializeRealisticFork(MAINNET_FORK); + vm.startPrank(owner); + roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), op_admin); + vm.stopPrank(); + etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); + etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); + etherFiWithdrawalBufferInstance.initialize(1e4, 1_00, 10_00); // 10% fee split to treasury, 1% exit fee, 10% low watermark } function test_rate_limit() public { @@ -77,14 +88,38 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertEq(address(user).balance, userBalance + 0.99 ether); assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), totalRedeemableAmount - 1 ether); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 10 ether); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(10 ether, user, user); + etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); + + vm.stopPrank(); + } + + function test_mainnet_redeem_weEth_with_rebase() public { + vm.deal(user, 100 ether); + + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(weEthInstance), 10 ether); + weEthInstance.wrap(10 ether); + vm.stopPrank(); + + uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 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.0101 ether); + assertEq(address(user).balance, userBalance + 0.9999 ether); vm.stopPrank(); } - function test_redeem_weEth() public { + function test_redeem_weEth_1() public { vm.deal(user, 100 ether); vm.startPrank(user); @@ -114,9 +149,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertEq(address(user).balance, userBalance + 0.99 ether); assertEq(etherFiWithdrawalBufferInstance.totalRedeemableAmount(), totalRedeemableAmount - 1 ether); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 10 ether); + eETHInstance.approve(address(weEthInstance), 6 ether); + weEthInstance.wrap(6 ether); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemWeEth(10 ether, user, user); + etherFiWithdrawalBufferInstance.redeemWeEth(5 ether, user, user); vm.stopPrank(); } @@ -142,4 +179,228 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertEq(address(user).balance, userBalance + (1.1 ether - 0.011 ether)); vm.stopPrank(); } + + // The test ensures that: + // - Redemption works correctly within allowed limits. + // - Fees are applied accurately. + // - The function properly reverts when redemption conditions aren't met. + function testFuzz_redeemEEth( + uint256 depositAmount, + uint256 redeemAmount, + uint256 exitFeeSplitBps, + uint16 exitFeeBps, + uint16 lowWatermarkBps + ) public { + depositAmount = bound(depositAmount, 1 ether, 1000 ether); + redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); + exitFeeSplitBps = bound(exitFeeSplitBps, 0, 10000); + exitFeeBps = uint16(bound(uint256(exitFeeBps), 0, 10000)); + lowWatermarkBps = uint16(bound(uint256(lowWatermarkBps), 0, 10000)); + + vm.deal(user, depositAmount); + vm.startPrank(user); + liquidityPoolInstance.deposit{value: depositAmount}(); + vm.stopPrank(); + + // Set exitFeeSplitToTreasuryInBps + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + + // Set exitFeeBasisPoints and lowWatermarkInBpsOfTvl + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + + vm.prank(owner); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + + vm.startPrank(user); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); + uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + + if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + uint256 userBalanceBefore = address(user).balance; + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); + + etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + + uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; + uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; + uint256 userReceives = redeemAmount - totalFee; + + assertApproxEqAbs( + eETHInstance.balanceOf(address(treasuryInstance)), + treasuryBalanceBefore + treasuryFee, + 1e1 + ); + assertApproxEqAbs( + address(user).balance, + userBalanceBefore + userReceives, + 1e1 + ); + + } else { + vm.expectRevert(); + etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + } + vm.stopPrank(); + } + + function testFuzz_redeemWeEth( + uint256 depositAmount, + uint256 redeemAmount, + uint256 exitFeeSplitBps, + uint16 exitFeeBps, + uint16 lowWatermarkBps + ) public { + // Bound the parameters + depositAmount = bound(depositAmount, 1 ether, 1000 ether); + redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); + exitFeeSplitBps = bound(exitFeeSplitBps, 0, 10000); + exitFeeBps = uint16(bound(uint256(exitFeeBps), 0, 10000)); + lowWatermarkBps = uint16(bound(uint256(lowWatermarkBps), 0, 10000)); + + // Deal Ether to user and perform deposit + vm.deal(user, depositAmount); + vm.startPrank(user); + liquidityPoolInstance.deposit{value: depositAmount}(); + vm.stopPrank(); + + // Set fee and watermark configurations + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + + vm.prank(owner); + etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + + vm.prank(owner); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + + // User approves weETH and attempts redemption + vm.startPrank(user); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); + uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + + if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + uint256 userBalanceBefore = address(user).balance; + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); + + etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + + uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; + uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; + uint256 userReceives = redeemAmount - totalFee; + + assertApproxEqAbs( + eETHInstance.balanceOf(address(treasuryInstance)), + treasuryBalanceBefore + treasuryFee, + 1e1 + ); + assertApproxEqAbs( + address(user).balance, + userBalanceBefore + userReceives, + 1e1 + ); + + } else { + vm.expectRevert(); + etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + } + vm.stopPrank(); + } + + function testFuzz_role_management(address admin, address pauser, address unpauser, address user) public { + address owner = roleRegistry.owner(); + bytes32 PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); + bytes32 PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); + bytes32 PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); + + vm.assume(admin != address(0) && admin != owner); + vm.assume(pauser != address(0) && pauser != owner && pauser != admin); + vm.assume(unpauser != address(0) && unpauser != owner && unpauser != admin && unpauser != pauser); + vm.assume(user != address(0) && user != owner && user != admin && user != pauser && user != unpauser); + + // Grant roles to respective addresses + vm.prank(owner); + roleRegistry.grantRole(PROTOCOL_ADMIN, admin); + vm.prank(owner); + roleRegistry.grantRole(PROTOCOL_PAUSER, pauser); + vm.prank(owner); + roleRegistry.grantRole(PROTOCOL_UNPAUSER, unpauser); + + // Admin performs admin-only actions + vm.startPrank(admin); + etherFiWithdrawalBufferInstance.setCapacity(10 ether); + etherFiWithdrawalBufferInstance.setRefillRatePerSecond(0.001 ether); + etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(1e4); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(1e2); + etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(1e2); + vm.stopPrank(); + + // Pauser pauses the contract + vm.startPrank(pauser); + etherFiWithdrawalBufferInstance.pauseContract(); + assertTrue(etherFiWithdrawalBufferInstance.paused()); + vm.stopPrank(); + + // Unpauser unpauses the contract + vm.startPrank(unpauser); + etherFiWithdrawalBufferInstance.unPauseContract(); + assertFalse(etherFiWithdrawalBufferInstance.paused()); + vm.stopPrank(); + + // Revoke PROTOCOL_ADMIN role from admin + vm.prank(owner); + roleRegistry.revokeRole(PROTOCOL_ADMIN, admin); + + // Admin attempts admin-only actions after role revocation + vm.startPrank(admin); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.setCapacity(10 ether); + vm.stopPrank(); + + // Pauser attempts to unpause (should fail) + vm.startPrank(pauser); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.unPauseContract(); + vm.stopPrank(); + + // Unpauser attempts to pause (should fail) + vm.startPrank(unpauser); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.pauseContract(); + vm.stopPrank(); + + // User without role attempts admin-only actions + vm.startPrank(user); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.pauseContract(); + vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); + etherFiWithdrawalBufferInstance.unPauseContract(); + vm.stopPrank(); + } + + function test_mainnet_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}(); + + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); + + assertEq(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + 0.01 ether); + assertEq(address(user).balance, userBalance + 0.99 ether); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); + vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); + + vm.stopPrank(); + } } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index af3eff876..00cdb85af 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -111,6 +111,7 @@ contract TestSetup is Test, ContractCodeChecker { UUPSProxy public etherFiWithdrawalBufferProxy; UUPSProxy public etherFiOracleProxy; UUPSProxy public etherFiAdminProxy; + UUPSProxy public roleRegistryProxy; DepositDataGeneration public depGen; IDepositContract public depositContractEth2; @@ -195,6 +196,8 @@ contract TestSetup is Test, ContractCodeChecker { EtherFiTimelock public etherFiTimelockInstance; BucketRateLimiter public bucketRateLimiter; + RoleRegistry public roleRegistry; + bytes32 root; bytes32 rootMigration; bytes32 rootMigration2; @@ -397,6 +400,7 @@ contract TestSetup is Test, ContractCodeChecker { etherFiTimelockInstance = EtherFiTimelock(payable(addressProviderInstance.getContractAddress("EtherFiTimelock"))); etherFiAdminInstance = EtherFiAdmin(payable(addressProviderInstance.getContractAddress("EtherFiAdmin"))); etherFiOracleInstance = EtherFiOracle(payable(addressProviderInstance.getContractAddress("EtherFiOracle"))); + roleRegistry = RoleRegistry(0x1d3Af47C1607A2EF33033693A9989D1d1013BB50); } function setUpLiquifier(uint8 forkEnum) internal { @@ -581,9 +585,15 @@ contract TestSetup is Test, ContractCodeChecker { etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); - etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance))), ""); + roleRegistryProxy = new UUPSProxy(address(new RoleRegistry()), ""); + roleRegistry = RoleRegistry(address(roleRegistryProxy)); + roleRegistry.initialize(owner); + + etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(0, 1_00, 10_00); + etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + + roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), owner); liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); From 77e61d8e7cfbb3ec5d22f0ab1efc9df9717d5f11 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 19 Dec 2024 16:38:40 +0900 Subject: [PATCH 47/95] use setter for 'shareRemainderSplitToTreasuryInBps', add more fuzz tests, add deploy script --- .../DeployEtherFiWithdrawalBuffer.s.sol | 40 +++ script/deploys/DeployPhaseTwo.s.sol | 2 +- .../WithdrawRequestNFTUpgradeScript.s.sol | 2 +- src/WithdrawRequestNFT.sol | 3 +- test/EtherFiWithdrawalBuffer.t.sol | 270 +++++++----------- test/TestSetup.sol | 2 +- test/WithdrawRequestNFT.t.sol | 254 ++++++++++++---- 7 files changed, 353 insertions(+), 220 deletions(-) create mode 100644 script/deploys/DeployEtherFiWithdrawalBuffer.s.sol diff --git a/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol new file mode 100644 index 000000000..34f132e0c --- /dev/null +++ b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; + +import "@openzeppelin/contracts/utils/Strings.sol"; + +import "../../src/Liquifier.sol"; +import "../../src/EtherFiRestaker.sol"; +import "../../src/helpers/AddressProvider.sol"; +import "../../src/UUPSProxy.sol"; +import "../../src/EtherFiWithdrawalBuffer.sol"; + + +contract Deploy is Script { + using Strings for string; + AddressProvider public addressProvider; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address addressProviderAddress = vm.envAddress("CONTRACT_REGISTRY"); + addressProvider = AddressProvider(addressProviderAddress); + + vm.startBroadcast(deployerPrivateKey); + + EtherFiWithdrawalBuffer impl = new EtherFiWithdrawalBuffer( + addressProvider.getContractAddress("LiquidityPool"), + addressProvider.getContractAddress("EETH"), + addressProvider.getContractAddress("WeETH"), + 0x0c83EAe1FE72c390A02E426572854931EefF93BA, // protocol safe + 0x1d3Af47C1607A2EF33033693A9989D1d1013BB50 // role registry + ); + UUPSProxy proxy = new UUPSProxy(payable(impl), ""); + + EtherFiWithdrawalBuffer instance = EtherFiWithdrawalBuffer(payable(proxy)); + instance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + + vm.stopBroadcast(); + } +} diff --git a/script/deploys/DeployPhaseTwo.s.sol b/script/deploys/DeployPhaseTwo.s.sol index 2417e191b..c61d13feb 100644 --- a/script/deploys/DeployPhaseTwo.s.sol +++ b/script/deploys/DeployPhaseTwo.s.sol @@ -81,7 +81,7 @@ contract DeployPhaseTwoScript is Script { } retrieve_contract_addresses(); - withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0), 0); + withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0)); withdrawRequestNftProxy = new UUPSProxy(address(withdrawRequestNftImplementation), ""); withdrawRequestNftInstance = WithdrawRequestNFT(payable(withdrawRequestNftProxy)); diff --git a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol index 5862fb36e..115251475 100644 --- a/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol +++ b/script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol @@ -20,7 +20,7 @@ contract WithdrawRequestNFTUpgrade is Script { vm.startBroadcast(deployerPrivateKey); WithdrawRequestNFT oracleInstance = WithdrawRequestNFT(proxyAddress); - WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(address(0), 0); + WithdrawRequestNFT v2Implementation = new WithdrawRequestNFT(address(0)); oracleInstance.upgradeTo(address(v2Implementation)); diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 439f0fa24..d3f6f29fe 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -38,9 +38,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad event HandledRemainderOfClaimedWithdrawRequests(uint256 eEthAmountToTreasury, uint256 eEthAmountBurnt); /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _treasury, uint16 _shareRemainderSplitToTreasuryInBps) { + constructor(address _treasury) { treasury = _treasury; - shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; _disableInitializers(); } diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol index d73268fd0..5073ed989 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -11,29 +11,30 @@ contract EtherFiWithdrawalBufferTest is TestSetup { 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 setUp_Fork() public { initializeRealisticFork(MAINNET_FORK); - vm.startPrank(owner); + vm.startPrank(roleRegistry.owner()); roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), op_admin); vm.stopPrank(); etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(1e4, 1_00, 10_00); // 10% fee split to treasury, 1% exit fee, 10% low watermark + etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + + _upgrade_liquidity_pool_contract(); + + vm.prank(liquidityPoolInstance.owner()); + liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); } function test_rate_limit() public { + vm.deal(user, 1000 ether); + vm.prank(user); + liquidityPoolInstance.deposit{value: 1000 ether}(); + assertEq(etherFiWithdrawalBufferInstance.canRedeem(1 ether), true); assertEq(etherFiWithdrawalBufferInstance.canRedeem(5 ether - 1), true); assertEq(etherFiWithdrawalBufferInstance.canRedeem(5 ether + 1), false); @@ -58,132 +59,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { 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), 5 ether); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); - - vm.stopPrank(); - } - - function test_mainnet_redeem_weEth_with_rebase() public { - vm.deal(user, 100 ether); - - vm.startPrank(user); - liquidityPoolInstance.deposit{value: 10 ether}(); - eETHInstance.approve(address(weEthInstance), 10 ether); - weEthInstance.wrap(10 ether); - vm.stopPrank(); - - uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; - - vm.prank(address(membershipManagerInstance)); - liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 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.0101 ether); - assertEq(address(user).balance, userBalance + 0.9999 ether); - vm.stopPrank(); - } - - function test_redeem_weEth_1() 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); - - eETHInstance.approve(address(weEthInstance), 6 ether); - weEthInstance.wrap(6 ether); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemWeEth(5 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(); + vm.expectRevert("INVALID"); + etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% } - // The test ensures that: - // - Redemption works correctly within allowed limits. - // - Fees are applied accurately. - // - The function properly reverts when redemption conditions aren't met. function testFuzz_redeemEEth( uint256 depositAmount, uint256 redeemAmount, @@ -214,13 +94,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); vm.startPrank(user); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); - - if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; @@ -230,12 +108,12 @@ contract EtherFiWithdrawalBufferTest is TestSetup { assertApproxEqAbs( eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalanceBefore + treasuryFee, - 1e1 + 1e2 ); assertApproxEqAbs( address(user).balance, userBalanceBefore + userReceives, - 1e1 + 1e2 ); } else { @@ -248,16 +126,18 @@ contract EtherFiWithdrawalBufferTest is TestSetup { function testFuzz_redeemWeEth( uint256 depositAmount, uint256 redeemAmount, - uint256 exitFeeSplitBps, + uint16 exitFeeSplitBps, + int256 rebase, uint16 exitFeeBps, uint16 lowWatermarkBps ) public { // Bound the parameters depositAmount = bound(depositAmount, 1 ether, 1000 ether); redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); - exitFeeSplitBps = bound(exitFeeSplitBps, 0, 10000); - exitFeeBps = uint16(bound(uint256(exitFeeBps), 0, 10000)); - lowWatermarkBps = uint16(bound(uint256(lowWatermarkBps), 0, 10000)); + exitFeeSplitBps = uint16(bound(exitFeeSplitBps, 0, 10000)); + exitFeeBps = uint16(bound(exitFeeBps, 0, 10000)); + lowWatermarkBps = uint16(bound(lowWatermarkBps, 0, 10000)); + rebase = bound(rebase, 0, int128(uint128(depositAmount) / 10)); // Deal Ether to user and perform deposit vm.deal(user, depositAmount); @@ -265,6 +145,10 @@ contract EtherFiWithdrawalBufferTest is TestSetup { liquidityPoolInstance.deposit{value: depositAmount}(); vm.stopPrank(); + // Apply rebase + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(int128(rebase)); + // Set fee and watermark configurations vm.prank(owner); etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); @@ -275,35 +159,39 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.prank(owner); etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); - // User approves weETH and attempts redemption + // Convert redeemAmount from ETH to weETH vm.startPrank(user); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - uint256 totalRedeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + eETHInstance.approve(address(weEthInstance), redeemAmount); + weEthInstance.wrap(redeemAmount); + uint256 weEthAmount = weEthInstance.balanceOf(user); - if (redeemAmount <= totalRedeemableAmount && etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); - etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); - uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), weEthAmount); + etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + + uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; - uint256 userReceives = redeemAmount - totalFee; + uint256 userReceives = eEthAmount - totalFee; assertApproxEqAbs( eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalanceBefore + treasuryFee, - 1e1 + 1e2 ); assertApproxEqAbs( address(user).balance, userBalanceBefore + userReceives, - 1e1 + 1e2 ); } else { vm.expectRevert(); - etherFiWithdrawalBufferInstance.redeemWeEth(redeemAmount, user, user); + etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); } vm.stopPrank(); } @@ -380,22 +268,26 @@ contract EtherFiWithdrawalBufferTest is TestSetup { } function test_mainnet_redeem_eEth() public { + setUp_Fork(); + 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}(); + liquidityPoolInstance.deposit{value: 10 ether}(); + uint256 redeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); - assertEq(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + 0.01 ether); - assertEq(address(user).balance, userBalance + 0.99 ether); + uint256 totalFee = (1 ether * 1e2) / 1e4; + uint256 treasuryFee = (totalFee * 1e3) / 1e4; + uint256 userReceives = 1 ether - totalFee; + + assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); @@ -403,4 +295,64 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.stopPrank(); } + + function test_mainnet_redeem_weEth_with_rebase() public { + setUp_Fork(); + + vm.deal(user, 100 ether); + + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(weEthInstance), 10 ether); + weEthInstance.wrap(1 ether); + vm.stopPrank(); + + uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; + + vm.prank(address(membershipManagerV1Instance)); + liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 10 eETH earned 1 ETH + + vm.startPrank(user); + uint256 weEthAmount = weEthInstance.balanceOf(user); + uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); + weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); + etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + + uint256 totalFee = (eEthAmount * 1e2) / 1e4; + uint256 treasuryFee = (totalFee * 1e3) / 1e4; + uint256 userReceives = eEthAmount - totalFee; + + assertApproxEqAbs(eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); + + vm.stopPrank(); + } + + function test_mainnet_redeem_beyond_liquidity_fails() public { + setUp_Fork(); + + uint256 redeemAmount = liquidityPoolInstance.getTotalPooledEther() / 2; + vm.prank(address(liquidityPoolInstance)); + eETHInstance.mintShares(user, 2 * redeemAmount); + + vm.startPrank(op_admin); + etherFiWithdrawalBufferInstance.setCapacity(2 * redeemAmount); + etherFiWithdrawalBufferInstance.setRefillRatePerSecond(2 * redeemAmount); + vm.stopPrank(); + + vm.warp(block.timestamp + 1); + + vm.startPrank(user); + + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + + eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); + vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + + vm.stopPrank(); + } } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 00cdb85af..0c9bbdd9c 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -564,7 +564,7 @@ contract TestSetup is Test, ContractCodeChecker { membershipNftProxy = new UUPSProxy(address(membershipNftImplementation), ""); membershipNftInstance = MembershipNFT(payable(membershipNftProxy)); - withdrawRequestNFTImplementation = new WithdrawRequestNFT(address(0), 0); + withdrawRequestNFTImplementation = new WithdrawRequestNFT(address(treasuryInstance)); withdrawRequestNFTProxy = new UUPSProxy(address(withdrawRequestNFTImplementation), ""); withdrawRequestNFTInstance = WithdrawRequestNFT(payable(withdrawRequestNFTProxy)); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 924545421..816d65424 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -14,60 +14,6 @@ contract WithdrawRequestNFTTest is TestSetup { setUpTests(); } - function test_WithdrawRequestNftInitializedCorrectly() public { - assertEq(address(withdrawRequestNFTInstance.liquidityPool()), address(liquidityPoolInstance)); - assertEq(address(withdrawRequestNFTInstance.eETH()), address(eETHInstance)); - } - - function test_RequestWithdraw() public { - startHoax(bob); - liquidityPoolInstance.deposit{value: 10 ether}(); - vm.stopPrank(); - - assertEq(liquidityPoolInstance.getTotalPooledEther(), 10 ether); - assertEq(eETHInstance.balanceOf(address(bob)), 10 ether); - - uint96 amountOfEEth = 1 ether; - - vm.prank(bob); - eETHInstance.approve(address(liquidityPoolInstance), amountOfEEth); - - vm.prank(bob); - uint256 requestId = liquidityPoolInstance.requestWithdraw(bob, amountOfEEth); - - WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); - - assertEq(request.amountOfEEth, 1 ether, "Amount of eEth should match"); - assertEq(request.shareOfEEth, 1 ether, "Share of eEth should match"); - assertTrue(request.isValid, "Request should be valid"); - } - - function test_RequestIdIncrements() public { - startHoax(bob); - liquidityPoolInstance.deposit{value: 10 ether}(); - vm.stopPrank(); - - assertEq(liquidityPoolInstance.getTotalPooledEther(), 10 ether); - - uint96 amountOfEEth = 1 ether; - - vm.prank(bob); - eETHInstance.approve(address(liquidityPoolInstance), amountOfEEth); - - vm.prank(bob); - uint256 requestId1 = liquidityPoolInstance.requestWithdraw(bob, amountOfEEth); - - assertEq(requestId1, 1, "Request id should be 1"); - - vm.prank(bob); - eETHInstance.approve(address(liquidityPoolInstance), amountOfEEth); - - vm.prank(bob); - uint256 requestId2 = liquidityPoolInstance.requestWithdraw(bob, amountOfEEth); - - assertEq(requestId2, 2, "Request id should be 2"); - } - function test_finalizeRequests() public { startHoax(bob); liquidityPoolInstance.deposit{value: 10 ether}(); @@ -183,6 +129,7 @@ contract WithdrawRequestNFTTest is TestSetup { assertEq(eETHInstance.balanceOf(bob), 9 ether); assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 1 ether, "eETH balance should be 1 ether"); + assertEq(eETHInstance.balanceOf(address(treasuryInstance)), 0 ether, "Treasury balance should be 0 ether"); // Rebase with accrued_rewards = 10 ether for the deposited 10 ether // -> 1 ether eETH shares = 2 ether ETH @@ -202,7 +149,7 @@ contract WithdrawRequestNFTTest is TestSetup { uint256 bobsEndingBalance = address(bob).balance; 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(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); } function test_ValidClaimWithdrawWithNegativeRebase() public { @@ -418,8 +365,203 @@ contract WithdrawRequestNFTTest is TestSetup { initializeRealisticFork(MAINNET_FORK); vm.startPrank(withdrawRequestNFTInstance.owner()); - withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner), 50_00))); + withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); + + withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds); } + + function testFuzz_RequestWithdraw(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { + // Assume valid conditions + vm.assume(depositAmount >= 1 ether && depositAmount <= 1000 ether); + vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount); + vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance)); + + // Setup initial balance for bob + vm.deal(bob, depositAmount); + + // Deposit ETH and get eETH + vm.startPrank(bob); + liquidityPoolInstance.deposit{value: depositAmount}(); + + // Approve and request withdraw + eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); + uint256 requestId = liquidityPoolInstance.requestWithdraw(recipient, withdrawAmount); + vm.stopPrank(); + + // Verify the request was created correctly + WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); + + assertEq(request.amountOfEEth, withdrawAmount, "Incorrect withdrawal amount"); + assertEq(request.shareOfEEth, liquidityPoolInstance.sharesForAmount(withdrawAmount), "Incorrect share amount"); + assertTrue(request.isValid, "Request should be valid"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), recipient, "Incorrect NFT owner"); + + // Verify eETH balances + assertEq(eETHInstance.balanceOf(bob), depositAmount - withdrawAmount, "Incorrect remaining eETH balance"); + assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), withdrawAmount, "Incorrect contract eETH balance"); + assertEq(withdrawRequestNFTInstance.nextRequestId(), requestId + 1, "Incorrect next request ID"); + + if (eETHInstance.balanceOf(bob) > 0) { + uint256 reqAmount = eETHInstance.balanceOf(bob); + vm.startPrank(bob); + eETHInstance.approve(address(liquidityPoolInstance), reqAmount); + uint256 requestId2 = liquidityPoolInstance.requestWithdraw(recipient, reqAmount); + vm.stopPrank(); + assertEq(requestId2, requestId + 1, "Incorrect next request ID"); + } + } + + function testFuzz_ClaimWithdraw( + uint96 depositAmount, + uint96 withdrawAmount, + uint96 rebaseAmount, + uint16 remainderSplitBps, + address recipient + ) public { + // Assume valid conditions + vm.assume(depositAmount >= 1 ether && depositAmount <= 1e6 ether); + vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount); + vm.assume(rebaseAmount >= 0 && rebaseAmount <= depositAmount); + vm.assume(remainderSplitBps <= 10000); + vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance)); + + // Setup initial balance for recipient + vm.deal(recipient, depositAmount); + + // Configure remainder split + vm.prank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(remainderSplitBps); + + // First deposit ETH to get eETH + vm.startPrank(recipient); + liquidityPoolInstance.deposit{value: depositAmount}(); + + // Record initial balances + uint256 treasuryEEthBefore = eETHInstance.balanceOf(address(treasuryInstance)); + uint256 recipientBalanceBefore = address(recipient).balance; + + // Request withdraw + eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); + uint256 requestId = liquidityPoolInstance.requestWithdraw(recipient, withdrawAmount); + vm.stopPrank(); + + // Get initial request state + WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); + + // Simulate rebase after request but before claim + vm.prank(address(membershipManagerInstance)); + liquidityPoolInstance.rebase(int128(uint128(rebaseAmount))); + + // Calculate expected withdrawal amounts after rebase + uint256 sharesValue = liquidityPoolInstance.amountForShare(request.shareOfEEth); + uint256 expectedWithdrawAmount = withdrawAmount < sharesValue ? withdrawAmount : sharesValue; + uint256 unusedShares = request.shareOfEEth - liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); + uint256 expectedTreasuryShares = (unusedShares * remainderSplitBps) / 10000; + uint256 expectedBurnedShares = request.shareOfEEth - expectedTreasuryShares; + assertGe(unusedShares, 0, "Unused shares should be non-negative because there was positive rebase"); + + // Track initial shares and total supply + uint256 initialTotalShares = eETHInstance.totalShares(); + + _finalizeWithdrawalRequest(requestId); + + vm.prank(recipient); + withdrawRequestNFTInstance.claimWithdraw(requestId); + + // Calculate expected burnt shares + uint256 burnedShares = initialTotalShares - eETHInstance.totalShares(); + + // Verify share burning + assertApproxEqAbs( + burnedShares, + expectedBurnedShares, + 1e1, + "Incorrect amount of shares burnt" + ); + assertLe(burnedShares, request.shareOfEEth, "Burned shares should be less than or equal to requested shares"); + + // Verify total supply reduction + assertApproxEqAbs( + eETHInstance.totalShares(), + initialTotalShares - burnedShares, + 1, + "Total shares not reduced correctly" + ); + assertGe( + eETHInstance.totalShares(), + initialTotalShares - burnedShares, + "Total shares should be greater than or equal to initial shares minus burned shares" + ); + + // Verify the withdrawal results + WithdrawRequestNFT.WithdrawRequest memory requestAfter = withdrawRequestNFTInstance.getRequest(requestId); + + // Request should be cleared + assertEq(requestAfter.amountOfEEth, 0, "Request should be cleared after claim"); + + // NFT should be burned + vm.expectRevert("ERC721: invalid token ID"); + withdrawRequestNFTInstance.ownerOf(requestId); + + // Calculate and verify remainder splitting + if (unusedShares > 0) { + assertApproxEqAbs( + eETHInstance.balanceOf(address(treasuryInstance)) - treasuryEEthBefore, + liquidityPoolInstance.amountForShare(expectedTreasuryShares), + 1e1, + "Incorrect treasury eETH balance" + ); + } + + // Verify recipient received correct ETH amount + assertEq( + address(recipient).balance, + recipientBalanceBefore + expectedWithdrawAmount, + "Recipient should receive correct ETH amount" + ); + } + + function testFuzz_InvalidateRequest(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { + // Assume valid conditions + vm.assume(depositAmount >= 1 ether && depositAmount <= 1000 ether); + vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount); + vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance) && !withdrawRequestNFTInstance.admins(recipient)); + + // Setup initial balance and deposit + vm.deal(recipient, depositAmount); + + vm.startPrank(recipient); + liquidityPoolInstance.deposit{value: depositAmount}(); + + // Request withdraw + eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); + uint256 requestId = liquidityPoolInstance.requestWithdraw(recipient, withdrawAmount); + vm.stopPrank(); + + // Verify request is initially valid + assertTrue(withdrawRequestNFTInstance.isValid(requestId), "Request should start valid"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), recipient, "Recipient should own NFT"); + + // Non-admin cannot invalidate + vm.prank(recipient); + vm.expectRevert("Caller is not the admin"); + withdrawRequestNFTInstance.invalidateRequest(requestId); + + // Admin invalidates request + vm.prank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.updateAdmin(recipient, true); + vm.prank(recipient); + withdrawRequestNFTInstance.invalidateRequest(requestId); + + // Verify request state after invalidation + assertFalse(withdrawRequestNFTInstance.isValid(requestId), "Request should be invalid"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), recipient, "NFT ownership should remain unchanged"); + + // Verify cannot transfer invalid request + vm.prank(recipient); + vm.expectRevert("INVALID_REQUEST"); + withdrawRequestNFTInstance.transferFrom(recipient, address(0xdead), requestId); + } } From 1435cc82e46dea589527016cbf35a1dab3a479ae Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 20 Dec 2024 10:24:20 +0900 Subject: [PATCH 48/95] handle issues in calculating the dust shares --- src/WithdrawRequestNFT.sol | 10 ++++++++-- test/WithdrawRequestNFT.t.sol | 27 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index d3f6f29fe..03e9b8b28 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -131,7 +131,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // 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 { + function handleAccumulatedShareRemainder(uint256[] memory _reqIds, uint256 _scanBegin) external onlyOwner { + assert (_scanBegin < nextRequestId); + bytes32 slot = keccak256("handleAccumulatedShareRemainder"); uint256 executed; assembly { @@ -141,9 +143,13 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint256 eEthSharesUnclaimedYet = 0; for (uint256 i = 0; i < _reqIds.length; i++) { - assert (_requests[_reqIds[i]].isValid); + if (!_requests[_reqIds[i]].isValid) continue; eEthSharesUnclaimedYet += _requests[_reqIds[i]].shareOfEEth; } + for (uint256 i = _scanBegin + 1; i < nextRequestId; i++) { + if (!_requests[i].isValid) continue; + eEthSharesUnclaimedYet += _requests[i].shareOfEEth; + } uint256 eEthSharesRemainder = eETH.shares(address(this)) - eEthSharesUnclaimedYet; handleRemainder(eEthSharesRemainder); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 816d65424..ed2c584d7 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -368,8 +368,31 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); - - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds); + vm.stopPrank(); + + // The goal is to count ALL dust shares that could be burnt in the past if we had the feature. + // Option 1 is to perform the off-chain calculation and input it as a parameter to the function, which is less transparent and not ideal + // Option 2 is to perform the calculation on-chain, which is more transparent but would require a lot of gas iterating for all CLAIMED requests + // -> The idea is to calculate the total eETH shares of ALL UNCLAIMED requests. + // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all CLAIMED requests. + // -> eETH.share(withdrawRequsetNFT) - Sum(request.shareOfEEth) for ALL unclaimed + + // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain: + // 1. When we queue up the txn, we will take a snapshot of ALL unclaimed requests and put their IDs as a parameter. + // 2. (issue) during the timelock period, there will be new requests that can't be included in the snapshot. + // the idea is to input last finalized request ID and scan from there to the latest request ID on-chain + uint256 scanBegin = withdrawRequestNFTInstance.lastFinalizedRequestId(); + + // If the request gets claimed during the timelock period, it will get skipped in the calculation. + vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[0])); + withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); + + vm.startPrank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); + vm.stopPrank(); + + vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[1])); + withdrawRequestNFTInstance.claimWithdraw(reqIds[1]); } function testFuzz_RequestWithdraw(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { From 8565d216d667d115c068f000acc9e65ac5436bcc Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 20 Dec 2024 10:26:11 +0900 Subject: [PATCH 49/95] improve comments --- test/WithdrawRequestNFT.t.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index ed2c584d7..939d4a705 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -374,10 +374,14 @@ contract WithdrawRequestNFTTest is TestSetup { // Option 1 is to perform the off-chain calculation and input it as a parameter to the function, which is less transparent and not ideal // Option 2 is to perform the calculation on-chain, which is more transparent but would require a lot of gas iterating for all CLAIMED requests // -> The idea is to calculate the total eETH shares of ALL UNCLAIMED requests. - // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all CLAIMED requests. + // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all UNCLAIMED requests. // -> eETH.share(withdrawRequsetNFT) - Sum(request.shareOfEEth) for ALL unclaimed - // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain: + // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain. + // One way is to iterate through all requests and sum up the shareOfEEth for all unclaimed requests. + // However, this would require a lot of gas and is not ideal. + // + // The idea is: // 1. When we queue up the txn, we will take a snapshot of ALL unclaimed requests and put their IDs as a parameter. // 2. (issue) during the timelock period, there will be new requests that can't be included in the snapshot. // the idea is to input last finalized request ID and scan from there to the latest request ID on-chain From 805bf07d53434a3f96b1a8dbd18b47282ef375f4 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 24 Dec 2024 09:27:38 +0900 Subject: [PATCH 50/95] add sorted & unique constraints + type change to reduce gas --- src/WithdrawRequestNFT.sol | 7 ++++++- test/WithdrawRequestNFT.t.sol | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 03e9b8b28..2a1da3b57 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -131,7 +131,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // 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, uint256 _scanBegin) external onlyOwner { + function handleAccumulatedShareRemainder(uint32[] memory _reqIds, uint256 _scanBegin) external onlyOwner { assert (_scanBegin < nextRequestId); bytes32 slot = keccak256("handleAccumulatedShareRemainder"); @@ -141,6 +141,11 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } require(executed == 0, "ALREADY_EXECUTED"); + // Check that _reqIds are sorted in ascending order and there is no duplication + for (uint256 i = 1; i < _reqIds.length; i++) { + require(_reqIds[i] > _reqIds[i - 1], "Entries must be sorted and unique"); + } + uint256 eEthSharesUnclaimedYet = 0; for (uint256 i = 0; i < _reqIds.length; i++) { if (!_requests[_reqIds[i]].isValid) continue; diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 939d4a705..f7d7ea8c6 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -8,7 +8,7 @@ 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]; + uint32[] public reqIds =[ 20, 388, 478, 714, 726, 729, 735, 815, 861, 916, 941, 1014, 1067, 1154, 1194, 1253]; function setUp() public { setUpTests(); @@ -392,6 +392,24 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); vm.startPrank(withdrawRequestNFTInstance.owner()); + uint32[] memory reqIdsWithIssues = new uint32[](4); + reqIdsWithIssues[0] = reqIds[0]; + reqIdsWithIssues[1] = reqIds[1]; + reqIdsWithIssues[2] = reqIds[3]; + reqIdsWithIssues[3] = reqIds[2]; + vm.expectRevert(); + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); + + reqIdsWithIssues[0] = reqIds[0]; + reqIdsWithIssues[1] = reqIds[1]; + reqIdsWithIssues[2] = reqIds[2]; + reqIdsWithIssues[3] = reqIds[2]; + vm.expectRevert(); + withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); + vm.stopPrank(); + + + vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); vm.stopPrank(); From 2de521a6d64d9c61b6c97c597c4f65dc98bb0b80 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 24 Dec 2024 10:05:15 +0900 Subject: [PATCH 51/95] update 'handleAccumulatedShareRemainder' to be callable by admin --- src/WithdrawRequestNFT.sol | 2 +- test/WithdrawRequestNFT.t.sol | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 2a1da3b57..58b851be8 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -131,7 +131,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // 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(uint32[] memory _reqIds, uint256 _scanBegin) external onlyOwner { + function handleAccumulatedShareRemainder(uint32[] memory _reqIds, uint256 _scanBegin) external onlyAdmin { assert (_scanBegin < nextRequestId); bytes32 slot = keccak256("handleAccumulatedShareRemainder"); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index f7d7ea8c6..4bd8bc627 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -364,6 +364,8 @@ contract WithdrawRequestNFTTest is TestSetup { function test_distributeImplicitFee() public { initializeRealisticFork(MAINNET_FORK); + address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); @@ -391,7 +393,7 @@ contract WithdrawRequestNFTTest is TestSetup { vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[0])); withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); - vm.startPrank(withdrawRequestNFTInstance.owner()); + vm.startPrank(etherfi_admin_wallet); uint32[] memory reqIdsWithIssues = new uint32[](4); reqIdsWithIssues[0] = reqIds[0]; reqIdsWithIssues[1] = reqIds[1]; @@ -406,10 +408,7 @@ contract WithdrawRequestNFTTest is TestSetup { reqIdsWithIssues[3] = reqIds[2]; vm.expectRevert(); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); - vm.stopPrank(); - - vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); vm.stopPrank(); From fc4a179ef68f1ec511ab856daf98e233daa0725a Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 26 Dec 2024 15:57:58 +0900 Subject: [PATCH 52/95] Certora audit: (1) add {aggregateSumEEthShareAmount}, (2) fix {_claimWithdraw} to account with 'totalLockedEEthShares', (3) simplify 'seizeRequest', (4) handle some issues --- src/EtherFiWithdrawalBuffer.sol | 32 +++--- src/WithdrawRequestNFT.sol | 165 +++++++++++++++-------------- test/EtherFiWithdrawalBuffer.t.sol | 9 ++ test/WithdrawRequestNFT.t.sol | 95 ++++++----------- 4 files changed, 143 insertions(+), 158 deletions(-) diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index a79b9e948..b51ed45b4 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -49,6 +49,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint16 public exitFeeInBps; uint16 public lowWatermarkInBpsOfTvl; // bps of TVL + event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); + receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor @@ -79,9 +81,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param eEthAmount The amount of eETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. * @param owner The address of the owner of the eETH. - * @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) { + function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); @@ -90,7 +91,7 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 afterEEthAmount = eEth.balanceOf(address(this)); uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - return _redeem(transferredEEthAmount, receiver); + _redeem(transferredEEthAmount, receiver); } /** @@ -98,9 +99,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param weEthAmount The amount of weETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. * @param owner The address of the owner of the weETH. - * @return The amount of ETH sent to the receiver and the exit fee amount. */ - function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant returns (uint256, uint256) { + function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { uint256 eEthShares = weEthAmount; uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); @@ -112,7 +112,7 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 afterEEthAmount = eEth.balanceOf(address(this)); uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - return _redeem(transferredEEthAmount, receiver); + _redeem(transferredEEthAmount, receiver); } @@ -120,9 +120,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @notice Redeems ETH. * @param ethAmount The amount of ETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. - * @return The amount of ETH sent to the receiver and the exit fee amount. */ - function _redeem(uint256 ethAmount, address receiver) internal returns (uint256, uint256) { + function _redeem(uint256 ethAmount, address receiver) internal { _updateRateLimit(ethAmount); uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); @@ -130,16 +129,18 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShareToReceiver); uint256 prevLpBalance = address(liquidityPool).balance; - uint256 prevBalance = address(this).balance; - uint256 burnedShares = (eEthAmountToReceiver > 0) ? liquidityPool.withdraw(address(this), eEthAmountToReceiver) : 0; - uint256 ethReceived = address(this).balance - prevBalance; + uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); - uint256 ethShareFee = ethShares - burnedShares; - uint256 eEthAmountFee = liquidityPool.amountForShare(ethShareFee); + uint256 ethShareFee = ethShares - sharesToBurn; uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + // Withdraw ETH from the liquidity pool + uint256 prevBalance = address(this).balance; + assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); + uint256 ethReceived = address(this).balance - prevBalance; + // To Stakers by burning shares eEth.burnShares(address(this), feeShareToStakers); @@ -148,9 +149,10 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); - require(success && address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Transfer failed"); + require(success, "EtherFiWithdrawalBuffer: Transfer failed"); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Invalid liquidity pool balance"); - return (ethReceived, eEthAmountFee); + emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } /** diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 58b851be8..91fa86325 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -30,13 +30,25 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 public lastFinalizedRequestId; uint16 public shareRemainderSplitToTreasuryInBps; + // inclusive + uint32 private _currentRequestIdToScanFromForShareRemainder; + uint32 private _lastRequestIdToScanUntilForShareRemainder; + + uint256 public totalLockedEEthShares; + + bool public paused; + address public pauser; + 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); + event Paused(address account); + event Unpaused(address account); + + /// @custom:oz-upgrades-unsafe-allow constructor constructor(address _treasury) { treasury = _treasury; @@ -57,6 +69,14 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad nextRequestId = 1; } + function initializeOnUpgrade(address _pauser) external onlyOwner { + paused = false; + pauser = _pauser; + + _currentRequestIdToScanFromForShareRemainder = 1; + _lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; + } + /// @notice creates a withdraw request and issues an associated NFT to the recipient /// @dev liquidity pool contract will call this function when a user requests withdraw /// @param amountOfEEth amount of eETH requested for withdrawal @@ -64,13 +84,15 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param recipient address to recieve with WithdrawRequestNFT /// @param fee fee to be subtracted from amount when recipient calls claimWithdraw /// @return uint256 id of the withdraw request - function requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) external payable onlyLiquidtyPool returns (uint256) { + function requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) external payable onlyLiquidtyPool whenNotPaused returns (uint256) { uint256 requestId = nextRequestId++; uint32 feeGwei = uint32(fee / 1 gwei); _requests[requestId] = IWithdrawRequestNFT.WithdrawRequest(amountOfEEth, shareOfEEth, true, feeGwei); _safeMint(recipient, requestId); + totalLockedEEthShares += shareOfEEth; + emit WithdrawRequestCreated(uint32(requestId), amountOfEEth, shareOfEEth, recipient, fee); return requestId; } @@ -93,7 +115,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @notice called by the NFT owner to claim their ETH /// @dev burns the NFT and transfers ETH from the liquidity pool to the owner minus any fee, withdraw request must be valid and finalized /// @param tokenId the id of the withdraw request and associated NFT - function claimWithdraw(uint256 tokenId) external { + function claimWithdraw(uint256 tokenId) external whenNotPaused { return _claimWithdraw(tokenId, ownerOf(tokenId)); } @@ -102,91 +124,48 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad IWithdrawRequestNFT.WithdrawRequest memory request = _requests[tokenId]; require(request.isValid, "Request is not valid"); - uint256 fee = uint256(request.feeGwei) * 1 gwei; uint256 amountToWithdraw = getClaimableAmount(tokenId); // transfer eth to recipient _burn(tokenId); delete _requests[tokenId]; + + uint256 shareAmountToBurnForWithdrawal = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); + totalLockedEEthShares -= shareAmountToBurnForWithdrawal; - uint256 amountBurnedShare = 0; - if (fee > 0) { - amountBurnedShare += liquidityPool.withdraw(address(membershipManager), fee); - } - amountBurnedShare += liquidityPool.withdraw(recipient, amountToWithdraw); - uint256 amountUnBurnedShare = request.shareOfEEth - amountBurnedShare; - handleRemainder(amountUnBurnedShare); + uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); + assert (amountBurnedShare == shareAmountToBurnForWithdrawal); - emit WithdrawRequestClaimed(uint32(tokenId), amountToWithdraw + fee, amountBurnedShare, recipient, fee); + emit WithdrawRequestClaimed(uint32(tokenId), amountToWithdraw, amountBurnedShare, recipient, 0); } - function batchClaimWithdraw(uint256[] calldata tokenIds) external { + function batchClaimWithdraw(uint256[] calldata tokenIds) external whenNotPaused { for (uint256 i = 0; i < tokenIds.length; i++) { _claimWithdraw(tokenIds[i], ownerOf(tokenIds[i])); } } - // 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(uint32[] memory _reqIds, uint256 _scanBegin) external onlyAdmin { - assert (_scanBegin < nextRequestId); - - bytes32 slot = keccak256("handleAccumulatedShareRemainder"); - uint256 executed; - assembly { - executed := sload(slot) - } - require(executed == 0, "ALREADY_EXECUTED"); + // This function is used to aggregate the sum of the eEth shares of the requests that have not been claimed yet. + // To be triggered during the upgrade to the new version of the contract. + function aggregateSumEEthShareAmount(uint256 _numReqsToScan) external { + // [scanFrom, scanUntil] + uint256 scanFrom = _currentRequestIdToScanFromForShareRemainder; + uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); - // Check that _reqIds are sorted in ascending order and there is no duplication - for (uint256 i = 1; i < _reqIds.length; i++) { - require(_reqIds[i] > _reqIds[i - 1], "Entries must be sorted and unique"); - } - - uint256 eEthSharesUnclaimedYet = 0; - for (uint256 i = 0; i < _reqIds.length; i++) { - if (!_requests[_reqIds[i]].isValid) continue; - eEthSharesUnclaimedYet += _requests[_reqIds[i]].shareOfEEth; - } - for (uint256 i = _scanBegin + 1; i < nextRequestId; i++) { + for (uint256 i = scanFrom; i <= scanUntil; i++) { if (!_requests[i].isValid) continue; - eEthSharesUnclaimedYet += _requests[i].shareOfEEth; + totalLockedEEthShares += _requests[i].shareOfEEth; } - uint256 eEthSharesRemainder = eETH.shares(address(this)) - eEthSharesUnclaimedYet; - handleRemainder(eEthSharesRemainder); - - assembly { - sstore(slot, 1) - executed := sload(slot) - } - assert (executed == 1); + _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); } - // Given an invalidated withdrawal request NFT of ID `requestId`:, - // - burn the NFT - // - withdraw its ETH to the `recipient` - function seizeInvalidRequest(uint256 requestId, address recipient) external onlyOwner { + // Seize the request simply by transferring it to another recipient + function seizeRequest(uint256 requestId, address recipient) external onlyOwner { require(!_requests[requestId].isValid, "Request is valid"); require(ownerOf(requestId) != address(0), "Already Claimed"); - // Bring the NFT to the `msg.sender` == contract owner - _transfer(ownerOf(requestId), owner(), requestId); - - // Undo its invalidation to claim - _requests[requestId].isValid = true; - - // its ETH amount is not locked - // - if it was finalized when being invalidated, we revoked it via `reduceEthAmountLockedForWithdrawal` - // - if it was not finalized when being invalidated, it was not locked - uint256 ethAmount = getClaimableAmount(requestId); - liquidityPool.addEthAmountLockedForWithdrawal(uint128(ethAmount)); - - // withdraw the ETH to the recipient - _claimWithdraw(requestId, recipient); + _transfer(ownerOf(requestId), recipient, requestId); emit WithdrawRequestSeized(uint32(requestId)); } @@ -221,13 +200,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad emit WithdrawRequestInvalidated(uint32(requestId)); } - function validateRequest(uint256 requestId) external onlyAdmin { - require(!_requests[requestId].isValid, "Request is valid"); - _requests[requestId].isValid = true; - - emit WithdrawRequestValidated(uint32(requestId)); - } - function updateAdmin(address _address, bool _isAdmin) external onlyOwner { require(_address != address(0), "Cannot be address zero"); admins[_address] = _isAdmin; @@ -237,30 +209,45 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; } + function pauseContract() external onlyPauser { + paused = true; + emit Paused(msg.sender); + } + + function unPauseContract() external onlyAdmin { + paused = false; + emit Unpaused(msg.sender); + } + /// @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); + /// @param _eEthAmount: the remainder of the eEth amount + function handleRemainder(uint256 _eEthAmount) external onlyAdmin { + require (getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); + + uint256 beforeEEthShares = eETH.shares(address(this)); + + uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(_eEthAmount); + uint256 eEthSharesToTreasury = eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); eETH.transfer(treasury, eEthAmountToTreasury); - uint256 eEthSharesToBurn = _eEthShares - eEthSharesToTreasury; + uint256 eEthSharesToBurn = eEthShares - eEthSharesToTreasury; eETH.burnShares(address(this), eEthSharesToBurn); + uint256 reducedEEthShares = beforeEEthShares - eETH.shares(address(this)); + totalLockedEEthShares -= reducedEEthShares; + 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++) { - uint256 tokenId = firstTokenId + i; - require(_requests[tokenId].isValid || msg.sender == owner(), "INVALID_REQUEST"); - } + function getEEthRemainderAmount() public view returns (uint256) { + uint256 eEthRemainderShare = eETH.shares(address(this)) - totalLockedEEthShares; + return liquidityPool.amountForShare(eEthRemainderShare); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} @@ -269,13 +256,27 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad return _getImplementation(); } + function _requireNotPaused() internal view virtual { + require(!paused, "Pausable: paused"); + } + modifier onlyAdmin() { require(admins[msg.sender], "Caller is not the admin"); _; } + modifier onlyPauser() { + require(msg.sender == pauser || admins[msg.sender] || msg.sender == owner(), "Caller is not the pauser"); + _; + } + modifier onlyLiquidtyPool() { require(msg.sender == address(liquidityPool), "Caller is not the liquidity pool"); _; } + + modifier whenNotPaused() { + _requireNotPaused(); + _; + } } diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiWithdrawalBuffer.t.sol index 5073ed989..7aede9fcf 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiWithdrawalBuffer.t.sol @@ -270,6 +270,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { function test_mainnet_redeem_eEth() public { setUp_Fork(); + vm.deal(alice, 50000 ether); + vm.prank(alice); + liquidityPoolInstance.deposit{value: 50000 ether}(); + + vm.deal(user, 100 ether); vm.startPrank(user); @@ -299,6 +304,10 @@ contract EtherFiWithdrawalBufferTest is TestSetup { function test_mainnet_redeem_weEth_with_rebase() public { setUp_Fork(); + vm.deal(alice, 50000 ether); + vm.prank(alice); + liquidityPoolInstance.deposit{value: 50000 ether}(); + vm.deal(user, 100 ether); vm.startPrank(user); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 4bd8bc627..07908ddf8 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -327,95 +327,68 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.transferFrom(alice, bob, requestId); } - function test_seizeInvalidAndMintNew_revert_if_not_owner() public { + function test_seizeRequest() public { uint256 requestId = test_InvalidatedRequestNft_after_finalization(); uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; // REVERT if not owner vm.prank(alice); vm.expectRevert("Ownable: caller is not the owner"); - withdrawRequestNFTInstance.seizeInvalidRequest(requestId, chad); - } - - function test_InvalidatedRequestNft_seizeInvalidAndMintNew_1() public { - uint256 requestId = test_InvalidatedRequestNft_after_finalization(); - uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; - uint256 chadBalance = address(chad).balance; + withdrawRequestNFTInstance.seizeRequest(requestId, chad); vm.prank(owner); - withdrawRequestNFTInstance.seizeInvalidRequest(requestId, chad); + withdrawRequestNFTInstance.seizeRequest(requestId, chad); assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); - assertEq(address(chad).balance, chadBalance + claimableAmount, "Chad should receive the claimable amount"); + assertEq(withdrawRequestNFTInstance.ownerOf(requestId), chad, "Chad should own the NFT"); } - function test_InvalidatedRequestNft_seizeInvalidAndMintNew_2() public { - uint256 requestId = test_InvalidatedRequestNft_before_finalization(); - uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; - uint256 chadBalance = address(chad).balance; - - vm.prank(owner); - withdrawRequestNFTInstance.seizeInvalidRequest(requestId, chad); - - assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); - assertEq(address(chad).balance, chadBalance + claimableAmount, "Chad should receive the claimable amount"); - } - function test_distributeImplicitFee() public { + function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; vm.startPrank(withdrawRequestNFTInstance.owner()); + // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); - + withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet); withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); + withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); + + // 2. PAUSE + withdrawRequestNFTInstance.pauseContract(); vm.stopPrank(); - // The goal is to count ALL dust shares that could be burnt in the past if we had the feature. - // Option 1 is to perform the off-chain calculation and input it as a parameter to the function, which is less transparent and not ideal - // Option 2 is to perform the calculation on-chain, which is more transparent but would require a lot of gas iterating for all CLAIMED requests - // -> The idea is to calculate the total eETH shares of ALL UNCLAIMED requests. - // Then, we can calculate the dust shares as the difference between the total eETH shares and the total eETH shares of all UNCLAIMED requests. - // -> eETH.share(withdrawRequsetNFT) - Sum(request.shareOfEEth) for ALL unclaimed - - // Now the question is how to calculate the total eETH shares of all unclaimed requests on-chain. - // One way is to iterate through all requests and sum up the shareOfEEth for all unclaimed requests. - // However, this would require a lot of gas and is not ideal. - // - // The idea is: - // 1. When we queue up the txn, we will take a snapshot of ALL unclaimed requests and put their IDs as a parameter. - // 2. (issue) during the timelock period, there will be new requests that can't be included in the snapshot. - // the idea is to input last finalized request ID and scan from there to the latest request ID on-chain - uint256 scanBegin = withdrawRequestNFTInstance.lastFinalizedRequestId(); - - // If the request gets claimed during the timelock period, it will get skipped in the calculation. - vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[0])); - withdrawRequestNFTInstance.claimWithdraw(reqIds[0]); - - vm.startPrank(etherfi_admin_wallet); - uint32[] memory reqIdsWithIssues = new uint32[](4); - reqIdsWithIssues[0] = reqIds[0]; - reqIdsWithIssues[1] = reqIds[1]; - reqIdsWithIssues[2] = reqIds[3]; - reqIdsWithIssues[3] = reqIds[2]; - vm.expectRevert(); - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); - - reqIdsWithIssues[0] = reqIds[0]; - reqIdsWithIssues[1] = reqIds[1]; - reqIdsWithIssues[2] = reqIds[2]; - reqIdsWithIssues[3] = reqIds[2]; - vm.expectRevert(); - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIdsWithIssues, scanBegin); - - withdrawRequestNFTInstance.handleAccumulatedShareRemainder(reqIds, scanBegin); + vm.startPrank(etherfi_admin_wallet); + + // 3. AggSum + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); + // ... + + vm.stopPrank(); + + // 4. Unpause + vm.startPrank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.unPauseContract(); vm.stopPrank(); + // Back to normal vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[1])); withdrawRequestNFTInstance.claimWithdraw(reqIds[1]); } + function test_handleRemainder() public { + test_aggregateSumEEthShareAmount(); + + vm.startPrank(withdrawRequestNFTInstance.owner()); + + withdrawRequestNFTInstance.handleRemainder(1 ether); + + vm.stopPrank(); + } + function testFuzz_RequestWithdraw(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { // Assume valid conditions vm.assume(depositAmount >= 1 ether && depositAmount <= 1000 ether); From 8acd29183500b388b0a0c1b10e679c31c9de8767 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 30 Dec 2024 09:32:11 +0900 Subject: [PATCH 53/95] wip: to be amended --- src/WithdrawRequestNFT.sol | 8 ++++++-- test/WithdrawRequestNFT.t.sol | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 91fa86325..d858e827e 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -70,6 +70,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function initializeOnUpgrade(address _pauser) external onlyOwner { + require(pauser == address(0), "Already initialized"); + paused = false; pauser = _pauser; @@ -89,10 +91,10 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 feeGwei = uint32(fee / 1 gwei); _requests[requestId] = IWithdrawRequestNFT.WithdrawRequest(amountOfEEth, shareOfEEth, true, feeGwei); - _safeMint(recipient, requestId); - totalLockedEEthShares += shareOfEEth; + _safeMint(recipient, requestId); + emit WithdrawRequestCreated(uint32(requestId), amountOfEEth, shareOfEEth, recipient, fee); return requestId; } @@ -206,6 +208,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function updateShareRemainderSplitToTreasuryInBps(uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { + require(_shareRemainderSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; } @@ -227,6 +230,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { require (getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); + require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "Not all requests have been scanned"); uint256 beforeEEthShares = eETH.shares(address(this)); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 07908ddf8..2f5059976 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -458,6 +458,7 @@ contract WithdrawRequestNFTTest is TestSetup { // Record initial balances uint256 treasuryEEthBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 recipientBalanceBefore = address(recipient).balance; + uint256 initialTotalLockedEEthShares = withdrawRequestNFTInstance.totalLockedEEthShares(); // Request withdraw eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); @@ -467,6 +468,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Get initial request state WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); + assertEq(withdrawRequestNFTInstance.totalLockedEEthShares(), initialTotalLockedEEthShares + request.shareOfEEth, "Incorrect total locked shares"); + // Simulate rebase after request but before claim vm.prank(address(membershipManagerInstance)); liquidityPoolInstance.rebase(int128(uint128(rebaseAmount))); @@ -474,10 +477,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Calculate expected withdrawal amounts after rebase uint256 sharesValue = liquidityPoolInstance.amountForShare(request.shareOfEEth); uint256 expectedWithdrawAmount = withdrawAmount < sharesValue ? withdrawAmount : sharesValue; - uint256 unusedShares = request.shareOfEEth - liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); - uint256 expectedTreasuryShares = (unusedShares * remainderSplitBps) / 10000; - uint256 expectedBurnedShares = request.shareOfEEth - expectedTreasuryShares; - assertGe(unusedShares, 0, "Unused shares should be non-negative because there was positive rebase"); + uint256 expectedBurnedShares = liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); + uint256 expectedLockedShares = request.shareOfEEth - expectedBurnedShares; // Track initial shares and total supply uint256 initialTotalShares = eETHInstance.totalShares(); @@ -491,13 +492,14 @@ contract WithdrawRequestNFTTest is TestSetup { uint256 burnedShares = initialTotalShares - eETHInstance.totalShares(); // Verify share burning + assertLe(burnedShares, request.shareOfEEth, "Burned shares should be less than or equal to requested shares"); assertApproxEqAbs( burnedShares, expectedBurnedShares, - 1e1, + 1e3, "Incorrect amount of shares burnt" ); - assertLe(burnedShares, request.shareOfEEth, "Burned shares should be less than or equal to requested shares"); + // Verify total supply reduction assertApproxEqAbs( @@ -523,14 +525,12 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.ownerOf(requestId); // Calculate and verify remainder splitting - if (unusedShares > 0) { - assertApproxEqAbs( - eETHInstance.balanceOf(address(treasuryInstance)) - treasuryEEthBefore, - liquidityPoolInstance.amountForShare(expectedTreasuryShares), - 1e1, - "Incorrect treasury eETH balance" - ); - } + assertApproxEqAbs( + expectedLockedShares, + withdrawRequestNFTInstance.totalLockedEEthShares(), + 1e1, + "Incorrect locked eETH share" + ); // Verify recipient received correct ETH amount assertEq( From a7e974b27be1e290a9b5f4838d0a4a2621e17a79 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 30 Dec 2024 15:13:01 +0900 Subject: [PATCH 54/95] add simplified {invalidate, validate} request, fix unit tests --- src/WithdrawRequestNFT.sol | 33 ++++++++++++++++++-------- test/WithdrawRequestNFT.t.sol | 44 +++++++++-------------------------- 2 files changed, 34 insertions(+), 43 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index d858e827e..90b939b72 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -42,6 +42,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad 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); @@ -155,7 +156,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); for (uint256 i = scanFrom; i <= scanUntil; i++) { - if (!_requests[i].isValid) continue; + if (!_exists(i)) continue; totalLockedEEthShares += _requests[i].shareOfEEth; } @@ -165,7 +166,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // Seize the request simply by transferring it to another recipient function seizeRequest(uint256 requestId, address recipient) external onlyOwner { require(!_requests[requestId].isValid, "Request is valid"); - require(ownerOf(requestId) != address(0), "Already Claimed"); + require(_exists(requestId), "Request does not exist"); _transfer(ownerOf(requestId), recipient, requestId); @@ -181,7 +182,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function isValid(uint256 requestId) public view returns (bool) { - require(_exists(requestId), "Request does not exist"); + require(_exists(requestId), "Request does not exist11"); return _requests[requestId].isValid; } @@ -191,17 +192,19 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad function invalidateRequest(uint256 requestId) external onlyAdmin { require(isValid(requestId), "Request is not valid"); - - if (isFinalized(requestId)) { - uint256 ethAmount = getClaimableAmount(requestId); - liquidityPool.reduceEthAmountLockedForWithdrawal(uint128(ethAmount)); - } - _requests[requestId].isValid = false; emit WithdrawRequestInvalidated(uint32(requestId)); } + function validateRequest(uint256 requestId) external onlyAdmin { + require(_exists(requestId), "Request does not exist22"); + require(!_requests[requestId].isValid, "Request is valid"); + _requests[requestId].isValid = true; + + emit WithdrawRequestValidated(uint32(requestId)); + } + function updateAdmin(address _address, bool _isAdmin) external onlyOwner { require(_address != address(0), "Cannot be address zero"); admins[_address] = _isAdmin; @@ -229,7 +232,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require (getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); + require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "Not all requests have been scanned"); uint256 beforeEEthShares = eETH.shares(address(this)); @@ -254,6 +257,16 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad return liquidityPool.amountForShare(eEthRemainderShare); } + // the withdraw request NFT is transferrable + // - if the request is valid, it can be transferred by the owner of the NFT + // - if the request is invalid, it can be transferred only by the owner of the WithdarwRequestNFT contract + function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { + for (uint256 i = 0; i < batchSize; i++) { + uint256 tokenId = firstTokenId + i; + require(_requests[tokenId].isValid || msg.sender == owner(), "INVALID_REQUEST"); + } + } + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} function getImplementation() external view returns (address) { diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 2f5059976..7e3e62944 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -65,7 +65,7 @@ contract WithdrawRequestNFTTest is TestSetup { assertTrue(request.isValid, "Request should be valid"); } - function testInvalidClaimWithdraw() public { + function test_InvalidClaimWithdraw() public { startHoax(bob); liquidityPoolInstance.deposit{value: 10 ether}(); vm.stopPrank(); @@ -117,7 +117,7 @@ contract WithdrawRequestNFTTest is TestSetup { assertEq(liquidityPoolInstance.getTotalPooledEther(), 10 ether); assertEq(eETHInstance.balanceOf(bob), 10 ether); - assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); + assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should start from 0 ether"); // Case 1. // Even after the rebase, the withdrawal amount should remain the same; 1 eth @@ -149,7 +149,7 @@ contract WithdrawRequestNFTTest is TestSetup { uint256 bobsEndingBalance = address(bob).balance; assertEq(bobsEndingBalance, bobsStartingBalance + 1 ether, "Bobs balance should be 1 ether higher"); - assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 0 ether, "eETH balance should be 0 ether"); + assertEq(eETHInstance.balanceOf(address(withdrawRequestNFTInstance)), 1 ether, "eETH balance should be 1 ether"); } function test_ValidClaimWithdrawWithNegativeRebase() public { @@ -319,31 +319,6 @@ contract WithdrawRequestNFTTest is TestSetup { _finalizeWithdrawalRequest(requestId); } - function test_InvalidatedRequestNft_NonTransferrable() public { - uint256 requestId = test_InvalidatedRequestNft_after_finalization(); - - vm.prank(alice); - vm.expectRevert("INVALID_REQUEST"); - withdrawRequestNFTInstance.transferFrom(alice, bob, requestId); - } - - function test_seizeRequest() public { - uint256 requestId = test_InvalidatedRequestNft_after_finalization(); - uint256 claimableAmount = withdrawRequestNFTInstance.getRequest(requestId).amountOfEEth; - - // REVERT if not owner - vm.prank(alice); - vm.expectRevert("Ownable: caller is not the owner"); - withdrawRequestNFTInstance.seizeRequest(requestId, chad); - - vm.prank(owner); - withdrawRequestNFTInstance.seizeRequest(requestId, chad); - - assertEq(liquidityPoolInstance.ethAmountLockedForWithdrawal(), 0, "Must be withdrawn"); - assertEq(withdrawRequestNFTInstance.ownerOf(requestId), chad, "Chad should own the NFT"); - } - - function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); @@ -363,8 +338,7 @@ contract WithdrawRequestNFTTest is TestSetup { vm.startPrank(etherfi_admin_wallet); // 3. AggSum - withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); - withdrawRequestNFTInstance.aggregateSumEEthShareAmount(1000); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); // ... vm.stopPrank(); @@ -383,7 +357,7 @@ contract WithdrawRequestNFTTest is TestSetup { test_aggregateSumEEthShareAmount(); vm.startPrank(withdrawRequestNFTInstance.owner()); - + vm.expectRevert("Not all requests have been scanned"); withdrawRequestNFTInstance.handleRemainder(1 ether); vm.stopPrank(); @@ -568,8 +542,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Admin invalidates request vm.prank(withdrawRequestNFTInstance.owner()); - withdrawRequestNFTInstance.updateAdmin(recipient, true); - vm.prank(recipient); + withdrawRequestNFTInstance.updateAdmin(admin, true); + vm.prank(admin); withdrawRequestNFTInstance.invalidateRequest(requestId); // Verify request state after invalidation @@ -580,5 +554,9 @@ contract WithdrawRequestNFTTest is TestSetup { vm.prank(recipient); vm.expectRevert("INVALID_REQUEST"); withdrawRequestNFTInstance.transferFrom(recipient, address(0xdead), requestId); + + // Owner can seize the invalidated request NFT + vm.prank(withdrawRequestNFTInstance.owner()); + withdrawRequestNFTInstance.seizeRequest(requestId, admin); } } From 83bcd4f360fdc85689bd4476f91c019b6dcbbcef Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Mon, 30 Dec 2024 18:09:49 +0900 Subject: [PATCH 55/95] rename EtherFiWithdrawBuffer -> EtherFiRedemptionManager --- script/deploys/DeployEtherFiRestaker.s.sol | 42 +++ .../DeployEtherFiWithdrawalBuffer.s.sol | 6 +- src/EtherFiRedemptionManager.sol | 280 ++++++++++++++++++ src/EtherFiWithdrawalBuffer.sol | 16 +- src/LiquidityPool.sol | 11 +- ...r.t.sol => EtherFiRedemptionManager.t.sol} | 136 ++++----- test/TestSetup.sol | 14 +- 7 files changed, 414 insertions(+), 91 deletions(-) create mode 100644 script/deploys/DeployEtherFiRestaker.s.sol create mode 100644 src/EtherFiRedemptionManager.sol rename test/{EtherFiWithdrawalBuffer.t.sol => EtherFiRedemptionManager.t.sol} (63%) diff --git a/script/deploys/DeployEtherFiRestaker.s.sol b/script/deploys/DeployEtherFiRestaker.s.sol new file mode 100644 index 000000000..0f4ec2225 --- /dev/null +++ b/script/deploys/DeployEtherFiRestaker.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; + +import "../../src/Liquifier.sol"; +import "../../src/EtherFiRestaker.sol"; +import "../../src/helpers/AddressProvider.sol"; +import "../../src/UUPSProxy.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +contract Deploy is Script { + using Strings for string; + + UUPSProxy public liquifierProxy; + + Liquifier public liquifierInstance; + + AddressProvider public addressProvider; + + address admin; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address addressProviderAddress = vm.envAddress("CONTRACT_REGISTRY"); + addressProvider = AddressProvider(addressProviderAddress); + + vm.startBroadcast(deployerPrivateKey); + + EtherFiRestaker restaker = EtherFiRestaker(payable(new UUPSProxy(payable(new EtherFiRestaker()), ""))); + restaker.initialize( + addressProvider.getContractAddress("LiquidityPool"), + addressProvider.getContractAddress("Liquifier") + ); + + new Liquifier(); + + // addressProvider.addContract(address(liquifierInstance), "Liquifier"); + + vm.stopBroadcast(); + } +} diff --git a/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol index 34f132e0c..ebc4050cf 100644 --- a/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol +++ b/script/deploys/DeployEtherFiWithdrawalBuffer.s.sol @@ -9,7 +9,7 @@ import "../../src/Liquifier.sol"; import "../../src/EtherFiRestaker.sol"; import "../../src/helpers/AddressProvider.sol"; import "../../src/UUPSProxy.sol"; -import "../../src/EtherFiWithdrawalBuffer.sol"; +import "../../src/EtherFiRedemptionManager.sol"; contract Deploy is Script { @@ -23,7 +23,7 @@ contract Deploy is Script { vm.startBroadcast(deployerPrivateKey); - EtherFiWithdrawalBuffer impl = new EtherFiWithdrawalBuffer( + EtherFiRedemptionManager impl = new EtherFiRedemptionManager( addressProvider.getContractAddress("LiquidityPool"), addressProvider.getContractAddress("EETH"), addressProvider.getContractAddress("WeETH"), @@ -32,7 +32,7 @@ contract Deploy is Script { ); UUPSProxy proxy = new UUPSProxy(payable(impl), ""); - EtherFiWithdrawalBuffer instance = EtherFiWithdrawalBuffer(payable(proxy)); + EtherFiRedemptionManager instance = EtherFiRedemptionManager(payable(proxy)); instance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); vm.stopBroadcast(); diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol new file mode 100644 index 000000000..c6c445c53 --- /dev/null +++ b/src/EtherFiRedemptionManager.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./interfaces/ILiquidityPool.sol"; +import "./interfaces/IeETH.sol"; +import "./interfaces/IWeETH.sol"; + +import "lib/BucketLimiter.sol"; + +import "./RoleRegistry.sol"; + +/* + The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. + - It has the exit fee as a percentage of the total amount redeemed. + - It has a rate limiter to limit the total amount that can be redeemed in a given time period. +*/ +contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 private constant BUCKET_UNIT_SCALE = 1e12; + uint256 private constant BASIS_POINT_SCALE = 1e4; + + bytes32 public constant PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); + bytes32 public constant PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); + bytes32 public constant PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); + + RoleRegistry public immutable roleRegistry; + address public immutable treasury; + IeETH public immutable eEth; + IWeETH public immutable weEth; + ILiquidityPool public immutable liquidityPool; + + BucketLimiter.Limit public limit; + uint16 public exitFeeSplitToTreasuryInBps; + uint16 public exitFeeInBps; + uint16 public lowWatermarkInBpsOfTvl; // bps of TVL + + event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); + + receive() external payable {} + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { + roleRegistry = RoleRegistry(_roleRegistry); + treasury = _treasury; + liquidityPool = ILiquidityPool(payable(_liquidityPool)); + eEth = IeETH(_eEth); + weEth = IWeETH(_weEth); + + _disableInitializers(); + } + + function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + exitFeeInBps = _exitFeeInBps; + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + /** + * @notice Redeems eETH for ETH. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the eETH. + */ + function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { + require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + _redeem(transferredEEthAmount, receiver); + } + + /** + * @notice Redeems weETH for ETH. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param owner The address of the owner of the weETH. + */ + function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { + uint256 eEthShares = weEthAmount; + uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); + require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); + weEth.unwrap(weEthAmount); + uint256 afterEEthAmount = eEth.balanceOf(address(this)); + + uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; + _redeem(transferredEEthAmount, receiver); + } + + + /** + * @notice Redeems ETH. + * @param ethAmount The amount of ETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + */ + function _redeem(uint256 ethAmount, address receiver) internal { + _updateRateLimit(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 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); + + uint256 ethShareFee = ethShares - sharesToBurn; + uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); + uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + + // Withdraw ETH from the liquidity pool + uint256 prevBalance = address(this).balance; + assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); + uint256 ethReceived = address(this).balance - prevBalance; + + // To Stakers by burning shares + eEth.burnShares(address(this), feeShareToStakers); + + // To Treasury by transferring eETH + IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + + // To Receiver by transferring ETH + (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); + require(success, "EtherFiRedemptionManager: Transfer failed"); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); + + emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); + } + + /** + * @dev if the contract has less than the low watermark, it will not allow any instant redemption. + */ + function lowWatermarkInETH() public view returns (uint256) { + return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + } + + /** + * @dev Returns the total amount that can be redeemed. + */ + function totalRedeemableAmount() external view returns (uint256) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { + return 0; + } + uint64 consumableBucketUnits = BucketLimiter.consumable(limit); + uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); + return Math.min(consumableAmount, liquidEthAmount); + } + + /** + * @dev Returns whether the given amount can be redeemed. + * @param amount The ETH or eETH amount to check. + */ + function canRedeem(uint256 amount) public view returns (bool) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { + return false; + } + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); + bool consumable = BucketLimiter.canConsume(limit, bucketUnit); + return consumable && amount <= liquidEthAmount; + } + + /** + * @dev Sets the maximum size of the bucket that can be consumed in a given time period. + * @param capacity The capacity of the bucket. + */ + function setCapacity(uint256 capacity) external hasRole(PROTOCOL_ADMIN) { + // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough + uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); + BucketLimiter.setCapacity(limit, bucketUnit); + } + + /** + * @dev Sets the rate at which the bucket is refilled per second. + * @param refillRate The rate at which the bucket is refilled per second. + */ + function setRefillRatePerSecond(uint256 refillRate) external hasRole(PROTOCOL_ADMIN) { + // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough + uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); + BucketLimiter.setRefillRate(limit, bucketUnit); + } + + /** + * @dev Sets the exit fee. + * @param _exitFeeInBps The exit fee. + */ + function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(PROTOCOL_ADMIN) { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeInBps = _exitFeeInBps; + } + + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(PROTOCOL_ADMIN) { + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(PROTOCOL_ADMIN) { + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + } + + function pauseContract() external hasRole(PROTOCOL_PAUSER) { + _pause(); + } + + function unPauseContract() external hasRole(PROTOCOL_UNPAUSER) { + _unpause(); + } + + function _updateRateLimit(uint256 amount) internal { + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); + require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); + } + + function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); + } + + function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { + return bucketUnit * BUCKET_UNIT_SCALE; + } + + /** + * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. + */ + // redeemable amount after exit fee + function previewRedeem(uint256 shares) public view returns (uint256) { + uint256 amountInEth = liquidityPool.amountForShare(shares); + return amountInEth - _fee(amountInEth, exitFeeInBps); + } + + 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 {} + + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + function _hasRole(bytes32 role, address account) internal view returns (bool) { + require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); + } + + modifier hasRole(bytes32 role) { + _hasRole(role, msg.sender); + _; + } + +} \ No newline at end of file diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol index b51ed45b4..c6c445c53 100644 --- a/src/EtherFiWithdrawalBuffer.sol +++ b/src/EtherFiWithdrawalBuffer.sol @@ -27,7 +27,7 @@ import "./RoleRegistry.sol"; - It has the exit fee as a percentage of the total amount redeemed. - It has a rate limiter to limit the total amount that can be redeemed in a given time period. */ -contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { +contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { using SafeERC20 for IERC20; using Math for uint256; @@ -83,8 +83,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU * @param owner The address of the owner of the eETH. */ function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); @@ -103,8 +103,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { uint256 eEthShares = weEthAmount; uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); - require(weEthAmount <= weEth.balanceOf(owner), "EtherFiWithdrawalBuffer: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); + require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); uint256 beforeEEthAmount = eEth.balanceOf(address(this)); IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); @@ -149,8 +149,8 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); - require(success, "EtherFiWithdrawalBuffer: Transfer failed"); - require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiWithdrawalBuffer: Invalid liquidity pool balance"); + require(success, "EtherFiRedemptionManager: Transfer failed"); + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } @@ -269,7 +269,7 @@ contract EtherFiWithdrawalBuffer is Initializable, OwnableUpgradeable, PausableU } function _hasRole(bytes32 role, address account) internal view returns (bool) { - require(roleRegistry.hasRole(role, account), "EtherFiWithdrawalBuffer: Unauthorized"); + require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); } modifier hasRole(bytes32 role) { diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 28ee11fdd..910dd05fe 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -23,7 +23,7 @@ import "./interfaces/IEtherFiAdmin.sol"; import "./interfaces/IAuctionManager.sol"; import "./interfaces/ILiquifier.sol"; -import "./EtherFiWithdrawalBuffer.sol"; +import "./EtherFiRedemptionManager.sol"; contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, ILiquidityPool { @@ -72,7 +72,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL bool private isLpBnftHolder; - EtherFiWithdrawalBuffer public etherFiWithdrawalBuffer; + EtherFiRedemptionManager public etherFiRedemptionManager; //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- @@ -144,8 +144,9 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL liquifier = ILiquifier(_liquifier); } - function initializeOnUpgradeWithWithdrawalBuffer(address _withdrawalBuffer) external onlyOwner { - etherFiWithdrawalBuffer = EtherFiWithdrawalBuffer(payable(_withdrawalBuffer)); + function initializeOnUpgradeWithRedemptionManager(address _etherFiRedemptionManager) external onlyOwner { + require(address(etherFiRedemptionManager) == address(0), "Already initialized"); + etherFiRedemptionManager = EtherFiRedemptionManager(payable(_etherFiRedemptionManager)); } // Used by eETH staking flow @@ -188,7 +189,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL /// it returns the amount of shares burned function withdraw(address _recipient, uint256 _amount) external whenNotPaused returns (uint256) { uint256 share = sharesForWithdrawalAmount(_amount); - require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiWithdrawalBuffer), "Incorrect Caller"); + require(msg.sender == address(withdrawRequestNFT) || msg.sender == address(membershipManager) || msg.sender == address(etherFiRedemptionManager), "Incorrect Caller"); if (totalValueInLp < _amount || (msg.sender == address(withdrawRequestNFT) && ethAmountLockedForWithdrawal < _amount) || eETH.balanceOf(msg.sender) < _amount) revert InsufficientLiquidity(); if (_amount > type(uint128).max || _amount == 0 || share == 0) revert InvalidAmount(); diff --git a/test/EtherFiWithdrawalBuffer.t.sol b/test/EtherFiRedemptionManager.t.sol similarity index 63% rename from test/EtherFiWithdrawalBuffer.t.sol rename to test/EtherFiRedemptionManager.t.sol index 7aede9fcf..f9f8da8e6 100644 --- a/test/EtherFiWithdrawalBuffer.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/console2.sol"; import "./TestSetup.sol"; -contract EtherFiWithdrawalBufferTest is TestSetup { +contract EtherFiRedemptionManagerTest is TestSetup { address user = vm.addr(999); address op_admin = vm.addr(1000); @@ -20,14 +20,14 @@ contract EtherFiWithdrawalBufferTest is TestSetup { roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), op_admin); vm.stopPrank(); - etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); - etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark _upgrade_liquidity_pool_contract(); vm.prank(liquidityPoolInstance.owner()); - liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); + liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); } function test_rate_limit() public { @@ -35,33 +35,33 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.prank(user); liquidityPoolInstance.deposit{value: 1000 ether}(); - 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); + assertEq(etherFiRedemptionManagerInstance.canRedeem(1 ether), true); + assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether - 1), true); + assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether + 1), false); + assertEq(etherFiRedemptionManagerInstance.canRedeem(10 ether), false); + assertEq(etherFiRedemptionManagerInstance.totalRedeemableAmount(), 5 ether); } function test_lowwatermark_guardrail() public { vm.deal(user, 100 ether); - assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 0 ether); + assertEq(etherFiRedemptionManagerInstance.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); + vm.startPrank(etherFiRedemptionManagerInstance.owner()); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00); // 1% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 1 ether); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(50_00); // 50% - assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 50 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(50_00); // 50% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 50 ether); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_00); // 100% - assertEq(etherFiWithdrawalBufferInstance.lowWatermarkInETH(), 100 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_00); // 100% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 100 ether); vm.expectRevert("INVALID"); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% } function testFuzz_redeemEEth( @@ -84,22 +84,22 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Set exitFeeSplitToTreasuryInBps vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); // Set exitFeeBasisPoints and lowWatermarkInBpsOfTvl vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps); vm.prank(owner); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); vm.startPrank(user); - if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -118,7 +118,7 @@ contract EtherFiWithdrawalBufferTest is TestSetup { } else { vm.expectRevert(); - etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); } vm.stopPrank(); } @@ -151,13 +151,13 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Set fee and watermark configurations vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); vm.prank(owner); - etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(exitFeeBps); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps); vm.prank(owner); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); // Convert redeemAmount from ETH to weETH vm.startPrank(user); @@ -165,14 +165,14 @@ contract EtherFiWithdrawalBufferTest is TestSetup { weEthInstance.wrap(redeemAmount); uint256 weEthAmount = weEthInstance.balanceOf(user); - if (etherFiWithdrawalBufferInstance.canRedeem(redeemAmount)) { + if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), weEthAmount); - etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + weEthInstance.approve(address(etherFiRedemptionManagerInstance), weEthAmount); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -191,7 +191,7 @@ contract EtherFiWithdrawalBufferTest is TestSetup { } else { vm.expectRevert(); - etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); } vm.stopPrank(); } @@ -217,23 +217,23 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Admin performs admin-only actions vm.startPrank(admin); - etherFiWithdrawalBufferInstance.setCapacity(10 ether); - etherFiWithdrawalBufferInstance.setRefillRatePerSecond(0.001 ether); - etherFiWithdrawalBufferInstance.setExitFeeSplitToTreasuryInBps(1e4); - etherFiWithdrawalBufferInstance.setLowWatermarkInBpsOfTvl(1e2); - etherFiWithdrawalBufferInstance.setExitFeeBasisPoints(1e2); + etherFiRedemptionManagerInstance.setCapacity(10 ether); + etherFiRedemptionManagerInstance.setRefillRatePerSecond(0.001 ether); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(1e4); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1e2); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(1e2); vm.stopPrank(); // Pauser pauses the contract vm.startPrank(pauser); - etherFiWithdrawalBufferInstance.pauseContract(); - assertTrue(etherFiWithdrawalBufferInstance.paused()); + etherFiRedemptionManagerInstance.pauseContract(); + assertTrue(etherFiRedemptionManagerInstance.paused()); vm.stopPrank(); // Unpauser unpauses the contract vm.startPrank(unpauser); - etherFiWithdrawalBufferInstance.unPauseContract(); - assertFalse(etherFiWithdrawalBufferInstance.paused()); + etherFiRedemptionManagerInstance.unPauseContract(); + assertFalse(etherFiRedemptionManagerInstance.paused()); vm.stopPrank(); // Revoke PROTOCOL_ADMIN role from admin @@ -242,28 +242,28 @@ contract EtherFiWithdrawalBufferTest is TestSetup { // Admin attempts admin-only actions after role revocation vm.startPrank(admin); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.setCapacity(10 ether); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.setCapacity(10 ether); vm.stopPrank(); // Pauser attempts to unpause (should fail) vm.startPrank(pauser); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.unPauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.unPauseContract(); vm.stopPrank(); // Unpauser attempts to pause (should fail) vm.startPrank(unpauser); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.pauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.pauseContract(); vm.stopPrank(); // User without role attempts admin-only actions vm.startPrank(user); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.pauseContract(); - vm.expectRevert("EtherFiWithdrawalBuffer: Unauthorized"); - etherFiWithdrawalBufferInstance.unPauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.pauseContract(); + vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); + etherFiRedemptionManagerInstance.unPauseContract(); vm.stopPrank(); } @@ -280,23 +280,23 @@ contract EtherFiWithdrawalBufferTest is TestSetup { liquidityPoolInstance.deposit{value: 10 ether}(); - uint256 redeemableAmount = etherFiWithdrawalBufferInstance.totalRedeemableAmount(); + uint256 redeemableAmount = etherFiRedemptionManagerInstance.totalRedeemableAmount(); uint256 userBalance = address(user).balance; - uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); - etherFiWithdrawalBufferInstance.redeemEEth(1 ether, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, user); uint256 totalFee = (1 ether * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; uint256 userReceives = 1 ether - totalFee; - assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())), treasuryBalance + treasuryFee, 1e1); assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), 5 ether); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(5 ether, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 5 ether); + vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); + etherFiRedemptionManagerInstance.redeemEEth(5 ether, user, user); vm.stopPrank(); } @@ -326,8 +326,8 @@ contract EtherFiWithdrawalBufferTest is TestSetup { uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); - weEthInstance.approve(address(etherFiWithdrawalBufferInstance), 1 ether); - etherFiWithdrawalBufferInstance.redeemWeEth(weEthAmount, user, user); + weEthInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); uint256 totalFee = (eEthAmount * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -347,8 +347,8 @@ contract EtherFiWithdrawalBufferTest is TestSetup { eETHInstance.mintShares(user, 2 * redeemAmount); vm.startPrank(op_admin); - etherFiWithdrawalBufferInstance.setCapacity(2 * redeemAmount); - etherFiWithdrawalBufferInstance.setRefillRatePerSecond(2 * redeemAmount); + etherFiRedemptionManagerInstance.setCapacity(2 * redeemAmount); + etherFiRedemptionManagerInstance.setRefillRatePerSecond(2 * redeemAmount); vm.stopPrank(); vm.warp(block.timestamp + 1); @@ -356,11 +356,11 @@ contract EtherFiWithdrawalBufferTest is TestSetup { vm.startPrank(user); uint256 userBalance = address(user).balance; - uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiWithdrawalBufferInstance.treasury())); + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); - eETHInstance.approve(address(etherFiWithdrawalBufferInstance), redeemAmount); - vm.expectRevert("EtherFiWithdrawalBuffer: Exceeded total redeemable amount"); - etherFiWithdrawalBufferInstance.redeemEEth(redeemAmount, user, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); + vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); vm.stopPrank(); } diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 0c9bbdd9c..76f687c72 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -49,7 +49,7 @@ import "../src/EtherFiAdmin.sol"; import "../src/EtherFiTimelock.sol"; import "../src/BucketRateLimiter.sol"; -import "../src/EtherFiWithdrawalBuffer.sol"; +import "../src/EtherFiRedemptionManager.sol"; import "../script/ContractCodeChecker.sol"; import "../script/Create2Factory.sol"; @@ -108,7 +108,7 @@ contract TestSetup is Test, ContractCodeChecker { UUPSProxy public membershipNftProxy; UUPSProxy public nftExchangeProxy; UUPSProxy public withdrawRequestNFTProxy; - UUPSProxy public etherFiWithdrawalBufferProxy; + UUPSProxy public etherFiRedemptionManagerProxy; UUPSProxy public etherFiOracleProxy; UUPSProxy public etherFiAdminProxy; UUPSProxy public roleRegistryProxy; @@ -169,7 +169,7 @@ contract TestSetup is Test, ContractCodeChecker { WithdrawRequestNFT public withdrawRequestNFTImplementation; WithdrawRequestNFT public withdrawRequestNFTInstance; - EtherFiWithdrawalBuffer public etherFiWithdrawalBufferInstance; + EtherFiRedemptionManager public etherFiRedemptionManagerInstance; NFTExchange public nftExchangeImplementation; NFTExchange public nftExchangeInstance; @@ -589,14 +589,14 @@ contract TestSetup is Test, ContractCodeChecker { roleRegistry = RoleRegistry(address(roleRegistryProxy)); roleRegistry.initialize(owner); - etherFiWithdrawalBufferProxy = new UUPSProxy(address(new EtherFiWithdrawalBuffer(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); - etherFiWithdrawalBufferInstance = EtherFiWithdrawalBuffer(payable(etherFiWithdrawalBufferProxy)); - etherFiWithdrawalBufferInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistry))), ""); + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); roleRegistry.grantRole(keccak256("PROTOCOL_ADMIN"), owner); liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); - liquidityPoolInstance.initializeOnUpgradeWithWithdrawalBuffer(address(etherFiWithdrawalBufferInstance)); + liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); membershipNftInstance.initialize("https://etherfi-cdn/{id}.json", address(membershipManagerInstance)); withdrawRequestNFTInstance.initialize(payable(address(liquidityPoolInstance)), payable(address(eETHInstance)), payable(address(membershipManagerInstance))); From 34f67b97ed4339ccf7e187074fe767a875d5b746 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 00:29:44 +0900 Subject: [PATCH 56/95] fix the logic to check the aggr calls --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 90b939b72..6a47c3c3b 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -233,7 +233,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); - require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "Not all requests have been scanned"); + require(_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); uint256 beforeEEthShares = eETH.shares(address(this)); From da5af14b387ccded04a25a012710fc2a22082013 Mon Sep 17 00:00:00 2001 From: Jacob T Firek <106350168+jtfirek@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:14:49 -0500 Subject: [PATCH 57/95] Update test/WithdrawRequestNFT.t.sol Signed-off-by: Jacob T Firek <106350168+jtfirek@users.noreply.github.com> --- test/WithdrawRequestNFT.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 7e3e62944..172362a37 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -357,7 +357,7 @@ contract WithdrawRequestNFTTest is TestSetup { test_aggregateSumEEthShareAmount(); vm.startPrank(withdrawRequestNFTInstance.owner()); - vm.expectRevert("Not all requests have been scanned"); + vm.expectRevert("Not all prev requests have been scanned"); withdrawRequestNFTInstance.handleRemainder(1 ether); vm.stopPrank(); From 577587b1f994da8acbd78aaefa5643fd37ebc947 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 07:48:47 +0900 Subject: [PATCH 58/95] reduce gas spending for 'call', update the upgrade init function, remove unused file --- src/EtherFiRedemptionManager.sol | 2 +- src/EtherFiWithdrawalBuffer.sol | 280 ------------------------------- src/WithdrawRequestNFT.sol | 4 +- test/WithdrawRequestNFT.t.sol | 3 +- 4 files changed, 5 insertions(+), 284 deletions(-) delete mode 100644 src/EtherFiWithdrawalBuffer.sol diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index c6c445c53..434e46f25 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -148,7 +148,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); // To Receiver by transferring ETH - (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); + (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); diff --git a/src/EtherFiWithdrawalBuffer.sol b/src/EtherFiWithdrawalBuffer.sol deleted file mode 100644 index c6c445c53..000000000 --- a/src/EtherFiWithdrawalBuffer.sol +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; -import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/math/Math.sol"; - -import "./interfaces/ILiquidityPool.sol"; -import "./interfaces/IeETH.sol"; -import "./interfaces/IWeETH.sol"; - -import "lib/BucketLimiter.sol"; - -import "./RoleRegistry.sol"; - -/* - The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. - - It has the exit fee as a percentage of the total amount redeemed. - - It has a rate limiter to limit the total amount that can be redeemed in a given time period. -*/ -contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { - using SafeERC20 for IERC20; - using Math for uint256; - - uint256 private constant BUCKET_UNIT_SCALE = 1e12; - uint256 private constant BASIS_POINT_SCALE = 1e4; - - bytes32 public constant PROTOCOL_PAUSER = keccak256("PROTOCOL_PAUSER"); - bytes32 public constant PROTOCOL_UNPAUSER = keccak256("PROTOCOL_UNPAUSER"); - bytes32 public constant PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN"); - - RoleRegistry public immutable roleRegistry; - address public immutable treasury; - IeETH public immutable eEth; - IWeETH public immutable weEth; - ILiquidityPool public immutable liquidityPool; - - BucketLimiter.Limit public limit; - uint16 public exitFeeSplitToTreasuryInBps; - uint16 public exitFeeInBps; - uint16 public lowWatermarkInBpsOfTvl; // bps of TVL - - event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); - - receive() external payable {} - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { - roleRegistry = RoleRegistry(_roleRegistry); - treasury = _treasury; - liquidityPool = ILiquidityPool(payable(_liquidityPool)); - eEth = IeETH(_eEth); - weEth = IWeETH(_weEth); - - _disableInitializers(); - } - - function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { - __Ownable_init(); - __UUPSUpgradeable_init(); - __Pausable_init(); - __ReentrancyGuard_init(); - - limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); - exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; - exitFeeInBps = _exitFeeInBps; - lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; - } - - /** - * @notice Redeems eETH for ETH. - * @param eEthAmount The amount of eETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the eETH. - */ - function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); - IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); - } - - /** - * @notice Redeems weETH for ETH. - * @param weEthAmount The amount of weETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the weETH. - */ - function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - uint256 eEthShares = weEthAmount; - uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); - require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); - IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); - weEth.unwrap(weEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); - } - - - /** - * @notice Redeems ETH. - * @param ethAmount The amount of ETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - */ - function _redeem(uint256 ethAmount, address receiver) internal { - _updateRateLimit(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 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); - - uint256 ethShareFee = ethShares - sharesToBurn; - uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); - uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; - - // Withdraw ETH from the liquidity pool - uint256 prevBalance = address(this).balance; - assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); - uint256 ethReceived = address(this).balance - prevBalance; - - // To Stakers by burning shares - eEth.burnShares(address(this), feeShareToStakers); - - // To Treasury by transferring eETH - IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - - // To Receiver by transferring ETH - (bool success, ) = receiver.call{value: ethReceived, gas: 100_000}(""); - require(success, "EtherFiRedemptionManager: Transfer failed"); - require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); - - emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); - } - - /** - * @dev if the contract has less than the low watermark, it will not allow any instant redemption. - */ - function lowWatermarkInETH() public view returns (uint256) { - return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); - } - - /** - * @dev Returns the total amount that can be redeemed. - */ - function totalRedeemableAmount() external view returns (uint256) { - uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); - if (liquidEthAmount < lowWatermarkInETH()) { - return 0; - } - uint64 consumableBucketUnits = BucketLimiter.consumable(limit); - uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); - return Math.min(consumableAmount, liquidEthAmount); - } - - /** - * @dev Returns whether the given amount can be redeemed. - * @param amount The ETH or eETH amount to check. - */ - function canRedeem(uint256 amount) public view returns (bool) { - uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); - if (liquidEthAmount < lowWatermarkInETH()) { - return false; - } - uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); - bool consumable = BucketLimiter.canConsume(limit, bucketUnit); - return consumable && amount <= liquidEthAmount; - } - - /** - * @dev Sets the maximum size of the bucket that can be consumed in a given time period. - * @param capacity The capacity of the bucket. - */ - function setCapacity(uint256 capacity) external hasRole(PROTOCOL_ADMIN) { - // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough - uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); - BucketLimiter.setCapacity(limit, bucketUnit); - } - - /** - * @dev Sets the rate at which the bucket is refilled per second. - * @param refillRate The rate at which the bucket is refilled per second. - */ - function setRefillRatePerSecond(uint256 refillRate) external hasRole(PROTOCOL_ADMIN) { - // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough - uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); - BucketLimiter.setRefillRate(limit, bucketUnit); - } - - /** - * @dev Sets the exit fee. - * @param _exitFeeInBps The exit fee. - */ - function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(PROTOCOL_ADMIN) { - require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); - exitFeeInBps = _exitFeeInBps; - } - - function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(PROTOCOL_ADMIN) { - require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); - lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; - } - - function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(PROTOCOL_ADMIN) { - require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); - exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; - } - - function pauseContract() external hasRole(PROTOCOL_PAUSER) { - _pause(); - } - - function unPauseContract() external hasRole(PROTOCOL_UNPAUSER) { - _unpause(); - } - - function _updateRateLimit(uint256 amount) internal { - uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); - require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); - } - - function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { - return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); - } - - function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { - return bucketUnit * BUCKET_UNIT_SCALE; - } - - /** - * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. - */ - // redeemable amount after exit fee - function previewRedeem(uint256 shares) public view returns (uint256) { - uint256 amountInEth = liquidityPool.amountForShare(shares); - return amountInEth - _fee(amountInEth, exitFeeInBps); - } - - 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 {} - - function getImplementation() external view returns (address) { - return _getImplementation(); - } - - function _hasRole(bytes32 role, address account) internal view returns (bool) { - require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); - } - - modifier hasRole(bytes32 role) { - _hasRole(role, msg.sender); - _; - } - -} \ No newline at end of file diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 6a47c3c3b..2ad797057 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -70,12 +70,14 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad nextRequestId = 1; } - function initializeOnUpgrade(address _pauser) external onlyOwner { + function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { require(pauser == address(0), "Already initialized"); paused = false; pauser = _pauser; + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; + _currentRequestIdToScanFromForShareRemainder = 1; _lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; } diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 172362a37..1c8b326c6 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -327,8 +327,7 @@ contract WithdrawRequestNFTTest is TestSetup { vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); - withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet); - withdrawRequestNFTInstance.updateShareRemainderSplitToTreasuryInBps(50_00); + withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet, 50_00); withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); // 2. PAUSE From 980af7d403b0c4441506cc048a489c9f451d6282 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 08:10:15 +0900 Subject: [PATCH 59/95] apply gas opt for BucketLimiter --- lib/BucketLimiter.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/BucketLimiter.sol b/lib/BucketLimiter.sol index 83f749156..36dc9a6ed 100644 --- a/lib/BucketLimiter.sol +++ b/lib/BucketLimiter.sol @@ -111,6 +111,11 @@ library BucketLimiter { function _refill(Limit memory limit) internal view { // We allow for overflow here, as the delta is resilient against it. uint64 now_ = uint64(block.timestamp); + + if (now_ == limit.lastRefill) { + return; + } + uint64 delta; unchecked { delta = now_ - limit.lastRefill; From 599d2874e739f4ee31b57591d2e8a9c4b0b6377a Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 08:32:15 +0900 Subject: [PATCH 60/95] improve assetion tsets, apply design pattern, function rename --- src/EtherFiRedemptionManager.sol | 7 +++++-- src/WithdrawRequestNFT.sol | 11 ++++++----- test/WithdrawRequestNFT.t.sol | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 434e46f25..a44a2eaa3 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -125,8 +125,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _updateRateLimit(ethAmount); uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); - uint256 ethShareToReceiver = ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE); - uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShareToReceiver); + uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver uint256 prevLpBalance = address(liquidityPool).balance; uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); @@ -137,6 +136,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; // Withdraw ETH from the liquidity pool + uint256 totalEEthShare = eEth.totalShares(); uint256 prevBalance = address(this).balance; assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); uint256 ethReceived = address(this).balance - prevBalance; @@ -150,7 +150,10 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); + + // Make sure the liquidity pool balance is correct && total shares are correct require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); + require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 2ad797057..27207a294 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -130,12 +130,13 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad require(request.isValid, "Request is not valid"); uint256 amountToWithdraw = getClaimableAmount(tokenId); + uint256 shareAmountToBurnForWithdrawal = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); // transfer eth to recipient _burn(tokenId); delete _requests[tokenId]; - - uint256 shareAmountToBurnForWithdrawal = liquidityPool.sharesForWithdrawalAmount(amountToWithdraw); + + // update accounting totalLockedEEthShares -= shareAmountToBurnForWithdrawal; uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); @@ -166,7 +167,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } // Seize the request simply by transferring it to another recipient - function seizeRequest(uint256 requestId, address recipient) external onlyOwner { + function seizeInvalidRequest(uint256 requestId, address recipient) external onlyOwner { require(!_requests[requestId].isValid, "Request is valid"); require(_exists(requestId), "Request does not exist"); @@ -184,7 +185,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function isValid(uint256 requestId) public view returns (bool) { - require(_exists(requestId), "Request does not exist11"); + require(_exists(requestId), "Request does not exist"); return _requests[requestId].isValid; } @@ -200,7 +201,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function validateRequest(uint256 requestId) external onlyAdmin { - require(_exists(requestId), "Request does not exist22"); + require(_exists(requestId), "Request does not exist"); require(!_requests[requestId].isValid, "Request is valid"); _requests[requestId].isValid = true; diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 1c8b326c6..a13d3f406 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -556,6 +556,6 @@ contract WithdrawRequestNFTTest is TestSetup { // Owner can seize the invalidated request NFT vm.prank(withdrawRequestNFTInstance.owner()); - withdrawRequestNFTInstance.seizeRequest(requestId, admin); + withdrawRequestNFTInstance.seizeInvalidRequest(requestId, admin); } } From 5bd66306d0c8f9cc99ebb6084228787f290c59fa Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 08:39:26 +0900 Subject: [PATCH 61/95] apply CEI pattern to 'handleRemainder' --- src/WithdrawRequestNFT.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 27207a294..0ac30749b 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -242,15 +242,16 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(_eEthAmount); uint256 eEthSharesToTreasury = eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); - eETH.transfer(treasury, eEthAmountToTreasury); - uint256 eEthSharesToBurn = eEthShares - eEthSharesToTreasury; + uint256 eEthSharesToMoved = eEthSharesToTreasury + eEthSharesToBurn; + + totalLockedEEthShares -= eEthSharesToMoved; + + eETH.transfer(treasury, eEthAmountToTreasury); eETH.burnShares(address(this), eEthSharesToBurn); - uint256 reducedEEthShares = beforeEEthShares - eETH.shares(address(this)); - totalLockedEEthShares -= reducedEEthShares; + require (beforeEEthShares - eEthSharesToMoved == eETH.shares(address(this)), "Invalid eETH shares after remainder handling"); emit HandledRemainderOfClaimedWithdrawRequests(eEthAmountToTreasury, liquidityPool.amountForShare(eEthSharesToBurn)); } From 2dd2115431662b2a9fc06879bafc4c025fe21f1e Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 31 Dec 2024 10:04:58 +0900 Subject: [PATCH 62/95] apply CEI pattern to 'redeem' --- src/EtherFiRedemptionManager.sol | 46 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index a44a2eaa3..6497c15a2 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -86,12 +86,11 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } /** @@ -101,18 +100,16 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param owner The address of the owner of the weETH. */ function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - uint256 eEthShares = weEthAmount; - uint256 eEthAmount = liquidityPool.amountForShare(eEthShares); + uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - uint256 beforeEEthAmount = eEth.balanceOf(address(this)); + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); weEth.unwrap(weEthAmount); - uint256 afterEEthAmount = eEth.balanceOf(address(this)); - uint256 transferredEEthAmount = afterEEthAmount - beforeEEthAmount; - _redeem(transferredEEthAmount, receiver); + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } @@ -121,23 +118,19 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param ethAmount The amount of ETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. */ - function _redeem(uint256 ethAmount, address receiver) internal { + function _redeem(uint256 ethAmount, uint256 eEthShares, address receiver, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) internal { _updateRateLimit(ethAmount); - uint256 ethShares = liquidityPool.sharesForAmount(ethAmount); - uint256 eEthAmountToReceiver = liquidityPool.amountForShare(ethShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver + // Derive additionals + uint256 eEthShareFee = eEthShares - sharesToBurn; + uint256 feeShareToStakers = eEthShareFee - feeShareToTreasury; + // Snapshot balances & shares for sanity check at the end + uint256 prevBalance = address(this).balance; uint256 prevLpBalance = address(liquidityPool).balance; - uint256 sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); - - uint256 ethShareFee = ethShares - sharesToBurn; - uint256 feeShareToTreasury = ethShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); - uint256 feeShareToStakers = ethShareFee - feeShareToTreasury; + uint256 totalEEthShare = eEth.totalShares(); // Withdraw ETH from the liquidity pool - uint256 totalEEthShare = eEth.totalShares(); - uint256 prevBalance = address(this).balance; assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); uint256 ethReceived = address(this).balance - prevBalance; @@ -252,6 +245,17 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable return bucketUnit * BUCKET_UNIT_SCALE; } + + function _calcRedemption(uint256 ethAmount) internal view returns (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) { + eEthShares = liquidityPool.sharesForAmount(ethAmount); + eEthAmountToReceiver = liquidityPool.amountForShare(eEthShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver + + sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); + uint256 eEthShareFee = eEthShares - sharesToBurn; + feeShareToTreasury = eEthShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); + } + /** * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. */ From fbcd9f1d6c32486190c2cb826b40b2fc8bfc86ca Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:29:04 +0900 Subject: [PATCH 63/95] use 'totalRemainderEEthShares' instead of locked share --- src/WithdrawRequestNFT.sol | 33 +++++++++++++++++++-------------- test/WithdrawRequestNFT.t.sol | 18 +++++++----------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 0ac30749b..540d2151e 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -33,8 +33,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // inclusive uint32 private _currentRequestIdToScanFromForShareRemainder; uint32 private _lastRequestIdToScanUntilForShareRemainder; + uint256 public _aggregateSumOfEEthShare; - uint256 public totalLockedEEthShares; + uint256 public totalRemainderEEthShares; bool public paused; address public pauser; @@ -94,7 +95,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 feeGwei = uint32(fee / 1 gwei); _requests[requestId] = IWithdrawRequestNFT.WithdrawRequest(amountOfEEth, shareOfEEth, true, feeGwei); - totalLockedEEthShares += shareOfEEth; _safeMint(recipient, requestId); @@ -137,7 +137,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad delete _requests[tokenId]; // update accounting - totalLockedEEthShares -= shareAmountToBurnForWithdrawal; + totalRemainderEEthShares += request.shareOfEEth - shareAmountToBurnForWithdrawal; uint256 amountBurnedShare = liquidityPool.withdraw(recipient, amountToWithdraw); assert (amountBurnedShare == shareAmountToBurnForWithdrawal); @@ -160,10 +160,17 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad for (uint256 i = scanFrom; i <= scanUntil; i++) { if (!_exists(i)) continue; - totalLockedEEthShares += _requests[i].shareOfEEth; + _aggregateSumOfEEthShare += _requests[i].shareOfEEth; } _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); + + // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` + if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { + require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "new req has been created"); + totalRemainderEEthShares = eETH.shares(address(this)) - _aggregateSumOfEEthShare; + _aggregateSumOfEEthShare = 0; // gone + } } // Seize the request simply by transferring it to another recipient @@ -235,18 +242,17 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); require(_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); + require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); uint256 beforeEEthShares = eETH.shares(address(this)); - - uint256 eEthShares = liquidityPool.sharesForWithdrawalAmount(_eEthAmount); - uint256 eEthSharesToTreasury = eEthShares.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); - uint256 eEthAmountToTreasury = liquidityPool.amountForShare(eEthSharesToTreasury); - uint256 eEthSharesToBurn = eEthShares - eEthSharesToTreasury; - uint256 eEthSharesToMoved = eEthSharesToTreasury + eEthSharesToBurn; - totalLockedEEthShares -= eEthSharesToMoved; + uint256 eEthAmountToTreasury = _eEthAmount.mulDiv(shareRemainderSplitToTreasuryInBps, BASIS_POINT_SCALE); + uint256 eEthAmountToBurn = _eEthAmount - eEthAmountToTreasury; + uint256 eEthSharesToBurn = liquidityPool.sharesForAmount(eEthAmountToBurn); + uint256 eEthSharesToMoved = eEthSharesToBurn + liquidityPool.sharesForAmount(eEthAmountToTreasury); + + totalRemainderEEthShares -= eEthSharesToMoved; eETH.transfer(treasury, eEthAmountToTreasury); eETH.burnShares(address(this), eEthSharesToBurn); @@ -257,8 +263,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function getEEthRemainderAmount() public view returns (uint256) { - uint256 eEthRemainderShare = eETH.shares(address(this)) - totalLockedEEthShares; - return liquidityPool.amountForShare(eEthRemainderShare); + return liquidityPool.amountForShare(totalRemainderEEthShares); } // the withdraw request NFT is transferrable diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index a13d3f406..da8874b71 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -417,6 +417,8 @@ contract WithdrawRequestNFTTest is TestSetup { vm.assume(remainderSplitBps <= 10000); vm.assume(recipient != address(0) && recipient != address(liquidityPoolInstance)); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(10); + // Setup initial balance for recipient vm.deal(recipient, depositAmount); @@ -431,7 +433,6 @@ contract WithdrawRequestNFTTest is TestSetup { // Record initial balances uint256 treasuryEEthBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 recipientBalanceBefore = address(recipient).balance; - uint256 initialTotalLockedEEthShares = withdrawRequestNFTInstance.totalLockedEEthShares(); // Request withdraw eETHInstance.approve(address(liquidityPoolInstance), withdrawAmount); @@ -441,8 +442,6 @@ contract WithdrawRequestNFTTest is TestSetup { // Get initial request state WithdrawRequestNFT.WithdrawRequest memory request = withdrawRequestNFTInstance.getRequest(requestId); - assertEq(withdrawRequestNFTInstance.totalLockedEEthShares(), initialTotalLockedEEthShares + request.shareOfEEth, "Incorrect total locked shares"); - // Simulate rebase after request but before claim vm.prank(address(membershipManagerInstance)); liquidityPoolInstance.rebase(int128(uint128(rebaseAmount))); @@ -497,20 +496,17 @@ contract WithdrawRequestNFTTest is TestSetup { vm.expectRevert("ERC721: invalid token ID"); withdrawRequestNFTInstance.ownerOf(requestId); - // Calculate and verify remainder splitting - assertApproxEqAbs( - expectedLockedShares, - withdrawRequestNFTInstance.totalLockedEEthShares(), - 1e1, - "Incorrect locked eETH share" - ); - // Verify recipient received correct ETH amount assertEq( address(recipient).balance, recipientBalanceBefore + expectedWithdrawAmount, "Recipient should receive correct ETH amount" ); + + uint256 expectedLockedEEthAmount = liquidityPoolInstance.amountForShare(expectedLockedShares); + withdrawRequestNFTInstance.getEEthRemainderAmount(); + vm.prank(admin); + withdrawRequestNFTInstance.handleRemainder(expectedLockedEEthAmount); } function testFuzz_InvalidateRequest(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { From 37306631ba3cb8477ef584c9574f65c470e3451a Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:30:36 +0900 Subject: [PATCH 64/95] initializeOnUpgrade cant be called twice --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 540d2151e..3ee26dce1 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -72,7 +72,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { - require(pauser == address(0), "Already initialized"); + require(_currentRequestIdToScanFromForShareRemainder == 0, "Already initialized"); paused = false; pauser = _pauser; From 72172e1b12b75b14edfda1be745a28622bf00314 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:44:45 +0900 Subject: [PATCH 65/95] initializeOnUpgrade onlyOnce --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 3ee26dce1..f97f65c13 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -72,7 +72,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { - require(_currentRequestIdToScanFromForShareRemainder == 0, "Already initialized"); + require(pauser == address(0) && _pauser != address(0), "Already initialized"); paused = false; pauser = _pauser; From 9dd6db1d4fb32b067ae5e7bf48e86aa16a18ace4 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:48:15 +0900 Subject: [PATCH 66/95] use uint256 instead of uint32 --- src/WithdrawRequestNFT.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index f97f65c13..3f46010bb 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -31,8 +31,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint16 public shareRemainderSplitToTreasuryInBps; // inclusive - uint32 private _currentRequestIdToScanFromForShareRemainder; - uint32 private _lastRequestIdToScanUntilForShareRemainder; + uint256 private _currentRequestIdToScanFromForShareRemainder; + uint256 private _lastRequestIdToScanUntilForShareRemainder; uint256 public _aggregateSumOfEEthShare; uint256 public totalRemainderEEthShares; @@ -163,7 +163,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad _aggregateSumOfEEthShare += _requests[i].shareOfEEth; } - _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); + _currentRequestIdToScanFromForShareRemainder = scanUntil + 1; // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { From b61899b6b21f09a3c27c3022df1805b708d606fe Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 07:50:14 +0900 Subject: [PATCH 67/95] revert --- src/WithdrawRequestNFT.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 3f46010bb..f97f65c13 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -31,8 +31,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint16 public shareRemainderSplitToTreasuryInBps; // inclusive - uint256 private _currentRequestIdToScanFromForShareRemainder; - uint256 private _lastRequestIdToScanUntilForShareRemainder; + uint32 private _currentRequestIdToScanFromForShareRemainder; + uint32 private _lastRequestIdToScanUntilForShareRemainder; uint256 public _aggregateSumOfEEthShare; uint256 public totalRemainderEEthShares; @@ -163,7 +163,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad _aggregateSumOfEEthShare += _requests[i].shareOfEEth; } - _currentRequestIdToScanFromForShareRemainder = scanUntil + 1; + _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { From 80c21bea8d23790efcb046b0505a9a6f6e8e98a2 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 08:10:18 +0900 Subject: [PATCH 68/95] improve the fuzz test --- test/WithdrawRequestNFT.t.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index da8874b71..5d354b7da 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -449,8 +449,8 @@ contract WithdrawRequestNFTTest is TestSetup { // Calculate expected withdrawal amounts after rebase uint256 sharesValue = liquidityPoolInstance.amountForShare(request.shareOfEEth); uint256 expectedWithdrawAmount = withdrawAmount < sharesValue ? withdrawAmount : sharesValue; - uint256 expectedBurnedShares = liquidityPoolInstance.sharesForWithdrawalAmount(expectedWithdrawAmount); - uint256 expectedLockedShares = request.shareOfEEth - expectedBurnedShares; + uint256 expectedBurnedShares = liquidityPoolInstance.sharesForAmount(expectedWithdrawAmount); + uint256 expectedDustShares = request.shareOfEEth - expectedBurnedShares; // Track initial shares and total supply uint256 initialTotalShares = eETHInstance.totalShares(); @@ -503,10 +503,17 @@ contract WithdrawRequestNFTTest is TestSetup { "Recipient should receive correct ETH amount" ); - uint256 expectedLockedEEthAmount = liquidityPoolInstance.amountForShare(expectedLockedShares); - withdrawRequestNFTInstance.getEEthRemainderAmount(); - vm.prank(admin); - withdrawRequestNFTInstance.handleRemainder(expectedLockedEEthAmount); + assertApproxEqAbs( + withdrawRequestNFTInstance.totalRemainderEEthShares(), + expectedDustShares, + 1, + "Incorrect remainder shares" + ); + + uint256 dustEEthAmount = withdrawRequestNFTInstance.getEEthRemainderAmount(); + vm.startPrank(admin); + withdrawRequestNFTInstance.handleRemainder(dustEEthAmount / 2); + withdrawRequestNFTInstance.handleRemainder(dustEEthAmount / 2); } function testFuzz_InvalidateRequest(uint96 depositAmount, uint96 withdrawAmount, address recipient) public { From 48626856c9d47d09218ad30649da932cc743cdfe Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 1 Jan 2025 19:51:10 +0900 Subject: [PATCH 69/95] (1) pause the contract on upgrade, (2) prevent from calling 'aggregateSumEEthShareAmount' again after the scan is completed, (3) prevent from undoing the finalization 'finalizeRequests' --- src/WithdrawRequestNFT.sol | 7 +++++-- test/WithdrawRequestNFT.t.sol | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index f97f65c13..759183ac4 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -73,8 +73,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad function initializeOnUpgrade(address _pauser, uint16 _shareRemainderSplitToTreasuryInBps) external onlyOwner { require(pauser == address(0) && _pauser != address(0), "Already initialized"); + require(_shareRemainderSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); - paused = false; + paused = true; // make sure the contract is paused after the upgrade pauser = _pauser; shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; @@ -154,6 +155,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // This function is used to aggregate the sum of the eEth shares of the requests that have not been claimed yet. // To be triggered during the upgrade to the new version of the contract. function aggregateSumEEthShareAmount(uint256 _numReqsToScan) external { + require(_currentRequestIdToScanFromForShareRemainder != _lastRequestIdToScanUntilForShareRemainder + 1, "scan is completed"); + // [scanFrom, scanUntil] uint256 scanFrom = _currentRequestIdToScanFromForShareRemainder; uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); @@ -167,7 +170,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { - require(_currentRequestIdToScanFromForShareRemainder == nextRequestId, "new req has been created"); totalRemainderEEthShares = eETH.shares(address(this)) - _aggregateSumOfEEthShare; _aggregateSumOfEEthShare = 0; // gone } @@ -197,6 +199,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function finalizeRequests(uint256 requestId) external onlyAdmin { + require(requestId > lastFinalizedRequestId, "Cannot undo finalization"); lastFinalizedRequestId = uint32(requestId); } diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 5d354b7da..21f5f31d0 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -419,6 +419,9 @@ contract WithdrawRequestNFTTest is TestSetup { withdrawRequestNFTInstance.aggregateSumEEthShareAmount(10); + vm.expectRevert("scan is completed"); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(10); + // Setup initial balance for recipient vm.deal(recipient, depositAmount); From f36c180b62676650046554b1d5e291f5c282f1a8 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 09:29:30 +0900 Subject: [PATCH 70/95] only owner of the funds can call {redeemEEth, redeemWeEth} --- src/EtherFiRedemptionManager.sol | 14 ++++++-------- test/EtherFiRedemptionManager.t.sol | 16 ++++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 6497c15a2..c5449096b 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -80,15 +80,14 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @notice Redeems eETH for ETH. * @param eEthAmount The amount of eETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the eETH. */ - function redeemEEth(uint256 eEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + function redeemEEth(uint256 eEthAmount, address receiver) public whenNotPaused nonReentrant { + require(eEthAmount <= eEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - IERC20(address(eEth)).safeTransferFrom(owner, address(this), eEthAmount); + IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), eEthAmount); _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } @@ -97,16 +96,15 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @notice Redeems weETH for ETH. * @param weEthAmount The amount of weETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. - * @param owner The address of the owner of the weETH. */ - function redeemWeEth(uint256 weEthAmount, address receiver, address owner) public whenNotPaused nonReentrant { + function redeemWeEth(uint256 weEthAmount, address receiver) public whenNotPaused nonReentrant { uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); - require(weEthAmount <= weEth.balanceOf(owner), "EtherFiRedemptionManager: Insufficient balance"); + require(weEthAmount <= weEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - IERC20(address(weEth)).safeTransferFrom(owner, address(this), weEthAmount); + IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), weEthAmount); weEth.unwrap(weEthAmount); _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index f9f8da8e6..6979fdf9c 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -99,7 +99,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -118,7 +118,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { } else { vm.expectRevert(); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); } vm.stopPrank(); } @@ -172,7 +172,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); weEthInstance.approve(address(etherFiRedemptionManagerInstance), weEthAmount); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -191,7 +191,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { } else { vm.expectRevert(); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); } vm.stopPrank(); } @@ -285,7 +285,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); - etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, user); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user); uint256 totalFee = (1 ether * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -296,7 +296,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { eETHInstance.approve(address(etherFiRedemptionManagerInstance), 5 ether); vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); - etherFiRedemptionManagerInstance.redeemEEth(5 ether, user, user); + etherFiRedemptionManagerInstance.redeemEEth(5 ether, user); vm.stopPrank(); } @@ -327,7 +327,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); weEthInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); uint256 totalFee = (eEthAmount * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -360,7 +360,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); vm.stopPrank(); } From 33567710798e86a1c5a9fc003f8cfeb069d972e9 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:13:32 +0900 Subject: [PATCH 71/95] disable unpause until the scan is completed --- src/WithdrawRequestNFT.sol | 36 ++++++++++-------- test/EtherFiRedemptionManager.t.sol | 12 ++++++ test/WithdrawRequestNFT.t.sol | 59 +++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 759183ac4..fd7fbb6a2 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -31,9 +31,9 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint16 public shareRemainderSplitToTreasuryInBps; // inclusive - uint32 private _currentRequestIdToScanFromForShareRemainder; - uint32 private _lastRequestIdToScanUntilForShareRemainder; - uint256 public _aggregateSumOfEEthShare; + uint32 public currentRequestIdToScanFromForShareRemainder; + uint32 public lastRequestIdToScanUntilForShareRemainder; + uint256 public aggregateSumOfEEthShare; uint256 public totalRemainderEEthShares; @@ -80,8 +80,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; - _currentRequestIdToScanFromForShareRemainder = 1; - _lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; + currentRequestIdToScanFromForShareRemainder = 1; + lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; } /// @notice creates a withdraw request and issues an associated NFT to the recipient @@ -155,23 +155,23 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad // This function is used to aggregate the sum of the eEth shares of the requests that have not been claimed yet. // To be triggered during the upgrade to the new version of the contract. function aggregateSumEEthShareAmount(uint256 _numReqsToScan) external { - require(_currentRequestIdToScanFromForShareRemainder != _lastRequestIdToScanUntilForShareRemainder + 1, "scan is completed"); + require(!isScanOfShareRemainderCompleted(), "scan is completed"); // [scanFrom, scanUntil] - uint256 scanFrom = _currentRequestIdToScanFromForShareRemainder; - uint256 scanUntil = Math.min(_lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); + uint256 scanFrom = currentRequestIdToScanFromForShareRemainder; + uint256 scanUntil = Math.min(lastRequestIdToScanUntilForShareRemainder, scanFrom + _numReqsToScan - 1); for (uint256 i = scanFrom; i <= scanUntil; i++) { if (!_exists(i)) continue; - _aggregateSumOfEEthShare += _requests[i].shareOfEEth; + aggregateSumOfEEthShare += _requests[i].shareOfEEth; } - _currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); + currentRequestIdToScanFromForShareRemainder = uint32(scanUntil + 1); - // When the scan is completed, update the `totalRemainderEEthShares` and reset the `_aggregateSumOfEEthShare` - if (_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1) { - totalRemainderEEthShares = eETH.shares(address(this)) - _aggregateSumOfEEthShare; - _aggregateSumOfEEthShare = 0; // gone + // When the scan is completed, update the `totalRemainderEEthShares` and reset the `aggregateSumOfEEthShare` + if (isScanOfShareRemainderCompleted()) { + totalRemainderEEthShares = eETH.shares(address(this)) - aggregateSumOfEEthShare; + aggregateSumOfEEthShare = 0; // gone } } @@ -234,6 +234,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function unPauseContract() external onlyAdmin { + require(isScanOfShareRemainderCompleted(), "scan is not completed"); + paused = false; emit Unpaused(msg.sender); } @@ -245,7 +247,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require(_currentRequestIdToScanFromForShareRemainder == _lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); + require(currentRequestIdToScanFromForShareRemainder == lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); uint256 beforeEEthShares = eETH.shares(address(this)); @@ -269,6 +271,10 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad return liquidityPool.amountForShare(totalRemainderEEthShares); } + function isScanOfShareRemainderCompleted() public view returns (bool) { + return currentRequestIdToScanFromForShareRemainder == (lastRequestIdToScanUntilForShareRemainder + 1); + } + // the withdraw request NFT is transferrable // - if the request is valid, it can be transferred by the owner of the NFT // - if the request is invalid, it can be transferred only by the owner of the WithdarwRequestNFT contract diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index 6979fdf9c..cd3f3cc10 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -30,6 +30,18 @@ contract EtherFiRedemptionManagerTest is TestSetup { liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); } + function test_upgrade_only_by_owner() public { + setUp_Fork(); + + address impl = etherFiRedemptionManagerInstance.getImplementation(); + vm.prank(admin); + vm.expectRevert(); + etherFiRedemptionManagerInstance.upgradeTo(impl); + + vm.prank(etherFiRedemptionManagerInstance.owner()); + etherFiRedemptionManagerInstance.upgradeTo(impl); + } + function test_rate_limit() public { vm.deal(user, 1000 ether); vm.prank(user); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 21f5f31d0..465dc4215 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -6,14 +6,35 @@ pragma solidity ^0.8.13; import "forge-std/console2.sol"; import "./TestSetup.sol"; + +contract WithdrawRequestNFTIntrusive is WithdrawRequestNFT { + + constructor() WithdrawRequestNFT(address(0)) {} + + function updateParam(uint32 _currentRequestIdToScanFromForShareRemainder, uint32 _lastRequestIdToScanUntilForShareRemainder) external { + currentRequestIdToScanFromForShareRemainder = _currentRequestIdToScanFromForShareRemainder; + lastRequestIdToScanUntilForShareRemainder = _lastRequestIdToScanUntilForShareRemainder; + } + +} + contract WithdrawRequestNFTTest is TestSetup { uint32[] public reqIds =[ 20, 388, 478, 714, 726, 729, 735, 815, 861, 916, 941, 1014, 1067, 1154, 1194, 1253]; + address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; function setUp() public { setUpTests(); } + function updateParam(uint32 _currentRequestIdToScanFromForShareRemainder, uint32 _lastRequestIdToScanUntilForShareRemainder) internal { + address cur_impl = withdrawRequestNFTInstance.getImplementation(); + address new_impl = address(new WithdrawRequestNFTIntrusive()); + withdrawRequestNFTInstance.upgradeTo(new_impl); + WithdrawRequestNFTIntrusive(address(withdrawRequestNFTInstance)).updateParam(_currentRequestIdToScanFromForShareRemainder, _lastRequestIdToScanUntilForShareRemainder); + withdrawRequestNFTInstance.upgradeTo(cur_impl); + } + function test_finalizeRequests() public { startHoax(bob); liquidityPoolInstance.deposit{value: 10 ether}(); @@ -322,42 +343,50 @@ contract WithdrawRequestNFTTest is TestSetup { function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); - address etherfi_admin_wallet = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; - vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet, 50_00); withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); - // 2. PAUSE - withdrawRequestNFTInstance.pauseContract(); - vm.stopPrank(); + // 2. (For test) update the scan range + updateParam(1, 200); - vm.startPrank(etherfi_admin_wallet); + // 2. Confirm Paused & Can't Unpause + assertTrue(withdrawRequestNFTInstance.paused(), "Contract should be paused"); + vm.expectRevert("scan is not completed"); + withdrawRequestNFTInstance.unPauseContract(); + vm.stopPrank(); // 3. AggSum + // - Can't Unpause untill the scan is not completed + // - Can't aggregateSumEEthShareAmount after the scan is completed withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); - // ... + assertFalse(withdrawRequestNFTInstance.isScanOfShareRemainderCompleted(), "Scan should be completed"); - vm.stopPrank(); + vm.prank(withdrawRequestNFTInstance.owner()); + vm.expectRevert("scan is not completed"); + withdrawRequestNFTInstance.unPauseContract(); + + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); + assertTrue(withdrawRequestNFTInstance.isScanOfShareRemainderCompleted(), "Scan should be completed"); - // 4. Unpause + vm.expectRevert("scan is completed"); + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(128); + + // 4. Can Unpause vm.startPrank(withdrawRequestNFTInstance.owner()); withdrawRequestNFTInstance.unPauseContract(); vm.stopPrank(); - // Back to normal - vm.prank(withdrawRequestNFTInstance.ownerOf(reqIds[1])); - withdrawRequestNFTInstance.claimWithdraw(reqIds[1]); + // we will run the test on the forked mainnet to perform the full scan and confirm we can unpause } function test_handleRemainder() public { test_aggregateSumEEthShareAmount(); - vm.startPrank(withdrawRequestNFTInstance.owner()); - vm.expectRevert("Not all prev requests have been scanned"); - withdrawRequestNFTInstance.handleRemainder(1 ether); + vm.prank(etherfi_admin_wallet); + withdrawRequestNFTInstance.handleRemainder(0.01 ether); vm.stopPrank(); } From ddd1b59b27cf980a813c8354d462e9e48d2a01e6 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:13:57 +0900 Subject: [PATCH 72/95] check the basis points params are below 1e4 --- src/EtherFiRedemptionManager.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index c5449096b..404f66e74 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -65,6 +65,10 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable } function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + __Ownable_init(); __UUPSUpgradeable_init(); __Pausable_init(); From 43a5a5e0941e5c5192bdb1e9c94eedeb178b6e09 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:16:39 +0900 Subject: [PATCH 73/95] disallow calling 'initializeOnUpgradeWithRedemptionManager' with invalid param & calling it twice --- src/LiquidityPool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 910dd05fe..622f80441 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -145,7 +145,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function initializeOnUpgradeWithRedemptionManager(address _etherFiRedemptionManager) external onlyOwner { - require(address(etherFiRedemptionManager) == address(0), "Already initialized"); + require(address(etherFiRedemptionManager) == address(0) && _etherFiRedemptionManager != address(0), "Invalid"); etherFiRedemptionManager = EtherFiRedemptionManager(payable(_etherFiRedemptionManager)); } From aa532803e8c5895d084183b5b50f9a04d3d905d2 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 10:23:15 +0900 Subject: [PATCH 74/95] (1) withdrawRequestNFT cannot call LiquidityPool.addEthAmountLockedForWithdrawal, (2) remove unused function reduceEthAmountLockedForWithdrawal --- src/LiquidityPool.sol | 8 +------- src/interfaces/ILiquidityPool.sol | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 622f80441..6fc71c1bc 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -532,17 +532,11 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function addEthAmountLockedForWithdrawal(uint128 _amount) external { - if (!(msg.sender == address(etherFiAdminContract) || msg.sender == address(withdrawRequestNFT))) revert IncorrectCaller(); + if (!(msg.sender == address(etherFiAdminContract))) revert IncorrectCaller(); ethAmountLockedForWithdrawal += _amount; } - function reduceEthAmountLockedForWithdrawal(uint128 _amount) external { - if (msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); - - ethAmountLockedForWithdrawal -= _amount; - } - //-------------------------------------------------------------------------------------- //------------------------------ INTERNAL FUNCTIONS ---------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index a71b2d6c7..9400955db 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -71,7 +71,6 @@ interface ILiquidityPool { function rebase(int128 _accruedRewards) external; function payProtocolFees(uint128 _protocolFees) external; function addEthAmountLockedForWithdrawal(uint128 _amount) external; - function reduceEthAmountLockedForWithdrawal(uint128 _amount) external; function setStakingTargetWeights(uint32 _eEthWeight, uint32 _etherFanWeight) external; function updateAdmin(address _newAdmin, bool _isAdmin) external; From 8ae484434deb4d017d2e6849b6e360e854948ef0 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 13:59:25 +0900 Subject: [PATCH 75/95] prevent finalizing future requests --- script/specialized/weEth_withdrawal_v2.s.sol | 69 ++++++++++++++++++++ src/WithdrawRequestNFT.sol | 1 + test/WithdrawRequestNFT.t.sol | 4 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 script/specialized/weEth_withdrawal_v2.s.sol diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol new file mode 100644 index 000000000..680028cde --- /dev/null +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "test/TestSetup.sol"; + +import "src/helpers/AddressProvider.sol"; + +contract Upgrade is Script { + + AddressProvider public addressProvider; + address public addressProviderAddress = 0x8487c5F8550E3C3e7734Fe7DCF77DB2B72E4A848; + address public roleRegistry = 0x1d3Af47C1607A2EF33033693A9989D1d1013BB50; + address public treasury = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + address public pauser = 0x9AF1298993DC1f397973C62A5D47a284CF76844D; + + WithdrawRequestNFT withdrawRequestNFTInstance; + LiquidityPool liquidityPoolInstance; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + AddressProvider addressProvider = AddressProvider(addressProviderAddress); + + withdrawRequestNFTInstance = WithdrawRequestNFT(payable(addressProvider.getContractAddress("WithdrawRequestNFT"))); + liquidityPoolInstance = LiquidityPool(payable(addressProvider.getContractAddress("LiquidityPool"))); + + vm.startBroadcast(deployerPrivateKey); + + // deploy_upgrade(); + // agg(); + // handle_remainder(); + + vm.stopBroadcast(); + } + + function deploy_upgrade() internal { + UUPSProxy etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager( + addressProvider.getContractAddress("LiquidityPool"), + addressProvider.getContractAddress("EETH"), + addressProvider.getContractAddress("WeETH"), + treasury, + roleRegistry)), ""); + EtherFiRedemptionManager etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + + withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(treasury))); + withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); // 50% fee split to treasury + + liquidityPoolInstance.upgradeTo(address(new LiquidityPool())); + liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); + } + + function agg() internal { + uint256 numToScanPerTx = 1024; + uint256 cnt = (withdrawRequestNFTInstance.nextRequestId() / numToScanPerTx) + 1; + console.log(cnt); + for (uint256 i = 0; i < cnt; i++) { + withdrawRequestNFTInstance.aggregateSumEEthShareAmount(numToScanPerTx); + } + } + + function handle_remainder() internal { + withdrawRequestNFTInstance.updateAdmin(msg.sender, true); + withdrawRequestNFTInstance.unPauseContract(); + uint256 remainder = withdrawRequestNFTInstance.getEEthRemainderAmount(); + console.log(remainder); + withdrawRequestNFTInstance.handleRemainder(remainder); + } +} \ No newline at end of file diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index fd7fbb6a2..cd03bcc1e 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -200,6 +200,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad function finalizeRequests(uint256 requestId) external onlyAdmin { require(requestId > lastFinalizedRequestId, "Cannot undo finalization"); + require(requestId < nextRequestId, "Cannot finalize future requests"); lastFinalizedRequestId = uint32(requestId); } diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 465dc4215..02954b842 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -343,10 +343,12 @@ contract WithdrawRequestNFTTest is TestSetup { function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); + address pauser = 0x9af1298993dc1f397973c62a5d47a284cf76844d; + vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(address(owner)))); - withdrawRequestNFTInstance.initializeOnUpgrade(etherfi_admin_wallet, 50_00); + withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); withdrawRequestNFTInstance.updateAdmin(etherfi_admin_wallet, true); // 2. (For test) update the scan range From 26b5a0beac01ff08032242f841540a487373e533 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 14:19:23 +0900 Subject: [PATCH 76/95] add {redeemEEthWithPermit, redeemWeEthWithPermit}, use try-catch for --- src/EtherFiRedemptionManager.sol | 9 +++++++++ src/LiquidityPool.sol | 2 +- src/interfaces/IWeETH.sol | 9 +++++++++ src/interfaces/IeETH.sol | 9 +++++++++ test/EtherFiRedemptionManager.t.sol | 10 +++++----- test/TestSetup.sol | 28 ++++++++++++++++++++++++++++ test/WithdrawRequestNFT.t.sol | 2 +- 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 404f66e74..5fc5fd8ed 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -114,6 +114,15 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } + function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external { + try eEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + redeemEEth(eEthAmount, receiver); + } + + function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external { + try weEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + redeemWeEth(weEthAmount, receiver); + } /** * @notice Redeems ETH. diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 6fc71c1bc..8a95400fa 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -235,7 +235,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL whenNotPaused returns (uint256) { - eETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s); + try eETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s) {} catch {} return requestWithdraw(_owner, _amount); } diff --git a/src/interfaces/IWeETH.sol b/src/interfaces/IWeETH.sol index b64d76ca3..4741da079 100644 --- a/src/interfaces/IWeETH.sol +++ b/src/interfaces/IWeETH.sol @@ -6,6 +6,15 @@ import "./ILiquidityPool.sol"; import "./IeETH.sol"; interface IWeETH is IERC20Upgradeable { + + struct PermitInput { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + // STATE VARIABLES function eETH() external view returns (IeETH); function liquidityPool() external view returns (ILiquidityPool); diff --git a/src/interfaces/IeETH.sol b/src/interfaces/IeETH.sol index 74fc6affa..f8ee974b9 100644 --- a/src/interfaces/IeETH.sol +++ b/src/interfaces/IeETH.sol @@ -2,6 +2,15 @@ pragma solidity ^0.8.13; interface IeETH { + + struct PermitInput { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + function name() external pure returns (string memory); function symbol() external pure returns (string memory); function decimals() external pure returns (uint8); diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index cd3f3cc10..7aa5374d4 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -109,9 +109,9 @@ contract EtherFiRedemptionManagerTest is TestSetup { if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); - - eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); + + IeETH.PermitInput memory permit = eEth_createPermitInput(999, address(etherFiRedemptionManagerInstance), redeemAmount, eETHInstance.nonces(user), 2**256 - 1, eETHInstance.DOMAIN_SEPARATOR()); + etherFiRedemptionManagerInstance.redeemEEthWithPermit(redeemAmount, user, permit); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -183,8 +183,8 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); - weEthInstance.approve(address(etherFiRedemptionManagerInstance), weEthAmount); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); + IWeETH.PermitInput memory permit = weEth_createPermitInput(999, address(etherFiRedemptionManagerInstance), weEthAmount, weEthInstance.nonces(user), 2**256 - 1, weEthInstance.DOMAIN_SEPARATOR()); + etherFiRedemptionManagerInstance.redeemWeEthWithPermit(weEthAmount, user, permit); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 76f687c72..e51ff0d35 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -1038,6 +1038,34 @@ contract TestSetup is Test, ContractCodeChecker { return permitInput; } + function eEth_createPermitInput(uint256 privKey, address spender, uint256 value, uint256 nonce, uint256 deadline, bytes32 domianSeparator) public returns (IeETH.PermitInput memory) { + address _owner = vm.addr(privKey); + bytes32 digest = calculatePermitDigest(_owner, spender, value, nonce, deadline, domianSeparator); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); + IeETH.PermitInput memory permitInput = IeETH.PermitInput({ + value: value, + deadline: deadline, + v: v, + r: r, + s: s + }); + return permitInput; + } + + function weEth_createPermitInput(uint256 privKey, address spender, uint256 value, uint256 nonce, uint256 deadline, bytes32 domianSeparator) public returns (IWeETH.PermitInput memory) { + address _owner = vm.addr(privKey); + bytes32 digest = calculatePermitDigest(_owner, spender, value, nonce, deadline, domianSeparator); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); + IWeETH.PermitInput memory permitInput = IWeETH.PermitInput({ + value: value, + deadline: deadline, + v: v, + r: r, + s: s + }); + return permitInput; + } + function registerAsBnftHolder(address _user) internal { (bool registered, uint32 index) = liquidityPoolInstance.bnftHoldersIndexes(_user); if (!registered) liquidityPoolInstance.registerAsBnftHolder(_user); diff --git a/test/WithdrawRequestNFT.t.sol b/test/WithdrawRequestNFT.t.sol index 02954b842..aeebdcb35 100644 --- a/test/WithdrawRequestNFT.t.sol +++ b/test/WithdrawRequestNFT.t.sol @@ -343,7 +343,7 @@ contract WithdrawRequestNFTTest is TestSetup { function test_aggregateSumEEthShareAmount() public { initializeRealisticFork(MAINNET_FORK); - address pauser = 0x9af1298993dc1f397973c62a5d47a284cf76844d; + address pauser = 0x9AF1298993DC1f397973C62A5D47a284CF76844D; vm.startPrank(withdrawRequestNFTInstance.owner()); // 1. Upgrade From a785f2511d4bc81628caf200eaae298d0a56f074 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 15:28:16 +0900 Subject: [PATCH 77/95] remove a redundant check --- src/WithdrawRequestNFT.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index cd03bcc1e..96a5213ad 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -104,7 +104,6 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad } function getClaimableAmount(uint256 tokenId) public view returns (uint256) { - require(tokenId < nextRequestId, "Request does not exist"); require(tokenId <= lastFinalizedRequestId, "Request is not finalized"); require(ownerOf(tokenId) != address(0), "Already Claimed"); From 878522476222f764675307f0b18d4208760f5ffb Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 18:10:42 +0900 Subject: [PATCH 78/95] Prevent 'initializeOnUpgrade' of LiquidityPool from being called twice, add the unused gap var and add initialization for added variables --- src/LiquidityPool.sol | 2 +- src/WithdrawRequestNFT.sol | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 8a95400fa..13f2d6e0f 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -138,7 +138,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function initializeOnUpgrade(address _auctionManager, address _liquifier) external onlyOwner { - require(_auctionManager != address(0) && _liquifier != address(0), "Invalid params"); + require(_auctionManager != address(0) && _liquifier != address(0) && address(auctionManager) == address(0) && address(liquifier) == address(0), "Invalid"); auctionManager = IAuctionManager(_auctionManager); liquifier = ILiquifier(_liquifier); diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 96a5213ad..0c3088cd5 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -29,6 +29,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad uint32 public nextRequestId; uint32 public lastFinalizedRequestId; uint16 public shareRemainderSplitToTreasuryInBps; + uint16 public _unused_gap; // inclusive uint32 public currentRequestIdToScanFromForShareRemainder; @@ -78,10 +79,15 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad paused = true; // make sure the contract is paused after the upgrade pauser = _pauser; + _unused_gap = 0; + shareRemainderSplitToTreasuryInBps = _shareRemainderSplitToTreasuryInBps; currentRequestIdToScanFromForShareRemainder = 1; lastRequestIdToScanUntilForShareRemainder = nextRequestId - 1; + + aggregateSumOfEEthShare = 0; + totalRemainderEEthShares = 0; } /// @notice creates a withdraw request and issues an associated NFT to the recipient From edc30cb09e8806689a99fa9e0f39545bab30745b Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 18:13:46 +0900 Subject: [PATCH 79/95] use 'isScanOfShareRemainderCompleted' --- src/WithdrawRequestNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index 0c3088cd5..a5fbeb413 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -253,7 +253,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { - require(currentRequestIdToScanFromForShareRemainder == lastRequestIdToScanUntilForShareRemainder + 1, "Not all prev requests have been scanned"); + require(isScanOfShareRemainderCompleted(), "Not all prev requests have been scanned"); require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); uint256 beforeEEthShares = eETH.shares(address(this)); From b666808a92581ad1eaec67fa3dd27ea783ece73f Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 20:40:37 +0900 Subject: [PATCH 80/95] add the max value constraint on rate limit --- src/EtherFiRedemptionManager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 5fc5fd8ed..9e9b97cd4 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -249,6 +249,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable } function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + require(amount <= type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); } From 23fd48b46297ceb02289f8c5f16fd608a152e5f9 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 2 Jan 2025 20:42:21 +0900 Subject: [PATCH 81/95] remove equality condition --- src/EtherFiRedemptionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 9e9b97cd4..630f959c4 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -249,7 +249,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable } function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { - require(amount <= type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); + require(amount < type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); } From 7a909acc0bffd81da44ded1eeefa0257402c1ed0 Mon Sep 17 00:00:00 2001 From: syko Date: Thu, 2 Jan 2025 23:41:16 +0900 Subject: [PATCH 82/95] fix the overflow issue in '_refill' Signed-off-by: syko --- lib/BucketLimiter.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/BucketLimiter.sol b/lib/BucketLimiter.sol index 36dc9a6ed..87a70a2f1 100644 --- a/lib/BucketLimiter.sol +++ b/lib/BucketLimiter.sol @@ -109,23 +109,22 @@ library BucketLimiter { } function _refill(Limit memory limit) internal view { - // We allow for overflow here, as the delta is resilient against it. uint64 now_ = uint64(block.timestamp); if (now_ == limit.lastRefill) { return; } - uint64 delta; + uint256 delta; unchecked { delta = now_ - limit.lastRefill; } - uint64 tokens = delta * limit.refillRate; - uint64 newRemaining = limit.remaining + tokens; + uint256 tokens = delta * uint256(limit.refillRate); + uint256 newRemaining = uint256(limit.remaining) + tokens; if (newRemaining > limit.capacity) { limit.remaining = limit.capacity; } else { - limit.remaining = newRemaining; + limit.remaining = uint64(newRemaining); } limit.lastRefill = now_; } @@ -167,4 +166,4 @@ library BucketLimiter { refill(limit); limit.remaining = remaining; } -} \ No newline at end of file +} From 0e6662b9f2cb3b71fbe809dcc1bc86971cdea3b9 Mon Sep 17 00:00:00 2001 From: syko Date: Thu, 2 Jan 2025 23:49:18 +0900 Subject: [PATCH 83/95] Update EtherFiRedemptionManager.sol Signed-off-by: syko --- src/EtherFiRedemptionManager.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 630f959c4..3969e624c 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -151,6 +151,8 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Treasury by transferring eETH IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); @@ -296,4 +298,4 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _; } -} \ No newline at end of file +} From f2cbc828a57c9be7637a5be5c84273d2bf7b5d74 Mon Sep 17 00:00:00 2001 From: syko Date: Fri, 3 Jan 2025 00:08:28 +0900 Subject: [PATCH 84/95] prevent the total shares from going below 1 gwei after redemption Signed-off-by: syko --- src/EtherFiRedemptionManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 3969e624c..5ff6b5cf7 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -151,7 +151,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Treasury by transferring eETH IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); @@ -159,7 +159,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // Make sure the liquidity pool balance is correct && total shares are correct require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); - require(eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } From d193b00c6fe49adbf1b65c1c6dcd1ed2eace9726 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Thu, 16 Jan 2025 14:27:56 -0500 Subject: [PATCH 85/95] fixes for Instant Withdrawal --- src/EtherFiRedemptionManager.sol | 80 ++++++++++++++++++++----------- src/LiquidityPool.sol | 5 ++ src/WithdrawRequestNFT.sol | 2 +- src/interfaces/ILiquidityPool.sol | 1 + 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 5ff6b5cf7..9ae29c850 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -86,14 +86,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param receiver The address to receive the redeemed ETH. */ function redeemEEth(uint256 eEthAmount, address receiver) public whenNotPaused nonReentrant { - require(eEthAmount <= eEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - - (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - - IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), eEthAmount); - - _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + _redeemEEth(msg.sender, eEthAmount, receiver); } /** @@ -102,26 +95,31 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param receiver The address to receive the redeemed ETH. */ function redeemWeEth(uint256 weEthAmount, address receiver) public whenNotPaused nonReentrant { - uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); - require(weEthAmount <= weEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - - (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - - IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), weEthAmount); - weEth.unwrap(weEthAmount); - - _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + _redeemWeEth(msg.sender, weEthAmount, receiver); } - function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external { - try eEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} - redeemEEth(eEthAmount, receiver); + /** + * @notice Redeems eETH for ETH with permit. + * @param owner The address of eETH owner. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param permit The permit params. + */ + function redeemEEthWithPermit(address owner, uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + try eEth.permit(owner, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + _redeemEEth(owner, eEthAmount, receiver); } - function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external { - try weEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} - redeemWeEth(weEthAmount, receiver); + /** + * @notice Redeems weETH for ETH. + * @param owner The address of weETH owner. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param permit The permit params. + */ + function redeemWeEthWithPermit(address owner, uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + try weEth.permit(owner, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + _redeemWeEth(owner, weEthAmount, receiver); } /** @@ -142,16 +140,17 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable uint256 totalEEthShare = eEth.totalShares(); // Withdraw ETH from the liquidity pool - assert (liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn); + require(liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn, "invalid num shares burnt"); uint256 ethReceived = address(this).balance - prevBalance; // To Stakers by burning shares - eEth.burnShares(address(this), feeShareToStakers); + liquidityPool.burnEEthShares(feeShareToStakers); // To Treasury by transferring eETH IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + uint256 totalShares = eEth.totalShares(); + require(totalShares >= 1 gwei && totalShares == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); @@ -159,7 +158,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // Make sure the liquidity pool balance is correct && total shares are correct require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); - require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + // require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); } @@ -245,6 +244,31 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _unpause(); } + function _redeemEEth(address user, uint256 eEthAmount, address receiver) internal { + require(eEthAmount <= eEth.balanceOf(user), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + + IERC20(address(eEth)).safeTransferFrom(user, address(this), eEthAmount); + + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + } + + function _redeemWeEth(address user, uint256 weEthAmount, address receiver) internal { + uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); + require(weEthAmount <= weEth.balanceOf(user), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + + IERC20(address(weEth)).safeTransferFrom(user, address(this), weEthAmount); + weEth.unwrap(weEthAmount); + + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + } + + function _updateRateLimit(uint256 amount) internal { uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 13f2d6e0f..50518d426 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -537,6 +537,11 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL ethAmountLockedForWithdrawal += _amount; } + function burnEEthShares(uint256 shares) external { + if (msg.sender != address(etherFiRedemptionManager) || msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); + eETH.burnShares(msg.sender, shares); + } + //-------------------------------------------------------------------------------------- //------------------------------ INTERNAL FUNCTIONS ---------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index a5fbeb413..dd7262079 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -266,7 +266,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad totalRemainderEEthShares -= eEthSharesToMoved; eETH.transfer(treasury, eEthAmountToTreasury); - eETH.burnShares(address(this), eEthSharesToBurn); + liquidityPool.burnEEthShares(eEthSharesToBurn); require (beforeEEthShares - eEthSharesToMoved == eETH.shares(address(this)), "Invalid eETH shares after remainder handling"); diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 9400955db..e56a03f53 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -76,4 +76,5 @@ interface ILiquidityPool { function updateAdmin(address _newAdmin, bool _isAdmin) external; function pauseContract() external; function unPauseContract() external; + function burnEEthShares(uint256 shares) external; } From 17aabaab623267c7443291cebe0b1c10e42ad15d Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Mon, 20 Jan 2025 14:27:51 -0500 Subject: [PATCH 86/95] fix: certora audit fixes --- script/specialized/weEth_withdrawal_v2.s.sol | 24 ++++++++++---- src/EtherFiRedemptionManager.sol | 34 +++++++++----------- src/LiquidityPool.sol | 15 ++++----- src/WithdrawRequestNFT.sol | 12 ++++--- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol index 680028cde..5f9933744 100644 --- a/script/specialized/weEth_withdrawal_v2.s.sol +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -39,15 +39,27 @@ contract Upgrade is Script { addressProvider.getContractAddress("EETH"), addressProvider.getContractAddress("WeETH"), treasury, - roleRegistry)), ""); + roleRegistry)), + abi.encodeWithSelector( + EtherFiRedemptionManager.initialize.selector, + 10_00, 1_00, 1_00, 5 ether, 0.001 ether // 10% fee split to treasury, 1% exit fee, 1% low watermark + ) + ); EtherFiRedemptionManager etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); - etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + // etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark - withdrawRequestNFTInstance.upgradeTo(address(new WithdrawRequestNFT(treasury))); - withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); // 50% fee split to treasury + withdrawRequestNFTInstance.upgradeToAndCall( + address(new WithdrawRequestNFT(treasury)), + abi.encodeWithSelector(WithdrawRequestNFT.initializeOnUpgrade.selector, pauser, 50_00) // 50% fee split to treasury + ); + // withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); // 50% fee split to treasury - liquidityPoolInstance.upgradeTo(address(new LiquidityPool())); - liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); + liquidityPoolInstance.upgradeToAndCall(address(new LiquidityPool()), abi.encodeWithSelector(LiquidityPool.initializeOnUpgradeWithRedemptionManager.selector, address(etherFiRedemptionManagerInstance))); + // liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); + + // verification + assert(withdrawRequestNFTInstance.pauser() == pauser); + assert(address(liquidityPoolInstance.etherFiRedemptionManager()) == address(etherFiRedemptionManagerInstance)); } function agg() internal { diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 9ae29c850..9584549cb 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -86,7 +86,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param receiver The address to receive the redeemed ETH. */ function redeemEEth(uint256 eEthAmount, address receiver) public whenNotPaused nonReentrant { - _redeemEEth(msg.sender, eEthAmount, receiver); + _redeemEEth(eEthAmount, receiver); } /** @@ -95,31 +95,29 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable * @param receiver The address to receive the redeemed ETH. */ function redeemWeEth(uint256 weEthAmount, address receiver) public whenNotPaused nonReentrant { - _redeemWeEth(msg.sender, weEthAmount, receiver); + _redeemWeEth(weEthAmount, receiver); } /** * @notice Redeems eETH for ETH with permit. - * @param owner The address of eETH owner. * @param eEthAmount The amount of eETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. * @param permit The permit params. */ - function redeemEEthWithPermit(address owner, uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { - try eEth.permit(owner, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} - _redeemEEth(owner, eEthAmount, receiver); + function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + try eEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + _redeemEEth(eEthAmount, receiver); } /** * @notice Redeems weETH for ETH. - * @param owner The address of weETH owner. * @param weEthAmount The amount of weETH to redeem after the exit fee. * @param receiver The address to receive the redeemed ETH. * @param permit The permit params. */ - function redeemWeEthWithPermit(address owner, uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { - try weEth.permit(owner, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} - _redeemWeEth(owner, weEthAmount, receiver); + function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + try weEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + _redeemWeEth(weEthAmount, receiver); } /** @@ -149,8 +147,8 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // To Treasury by transferring eETH IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - uint256 totalShares = eEth.totalShares(); - require(totalShares >= 1 gwei && totalShares == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + // uint256 totalShares = eEth.totalShares(); + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); // To Receiver by transferring ETH (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); @@ -244,25 +242,25 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable _unpause(); } - function _redeemEEth(address user, uint256 eEthAmount, address receiver) internal { - require(eEthAmount <= eEth.balanceOf(user), "EtherFiRedemptionManager: Insufficient balance"); + function _redeemEEth(uint256 eEthAmount, address receiver) internal { + require(eEthAmount <= eEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - IERC20(address(eEth)).safeTransferFrom(user, address(this), eEthAmount); + IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), eEthAmount); _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); } - function _redeemWeEth(address user, uint256 weEthAmount, address receiver) internal { + function _redeemWeEth(uint256 weEthAmount, address receiver) internal { uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); - require(weEthAmount <= weEth.balanceOf(user), "EtherFiRedemptionManager: Insufficient balance"); + require(weEthAmount <= weEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); - IERC20(address(weEth)).safeTransferFrom(user, address(this), weEthAmount); + IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), weEthAmount); weEth.unwrap(weEthAmount); _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 50518d426..58ebe0543 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -11,7 +11,6 @@ import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import "./interfaces/IRegulationsManager.sol"; -import "./interfaces/IStakingManager.sol"; import "./interfaces/IEtherFiNodesManager.sol"; import "./interfaces/IeETH.sol"; import "./interfaces/IStakingManager.sol"; @@ -27,6 +26,7 @@ import "./EtherFiRedemptionManager.sol"; contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, ILiquidityPool { + using SafeERC20 for IERC20; //-------------------------------------------------------------------------------------- //--------------------------------- STATE-VARIABLES ---------------------------------- //-------------------------------------------------------------------------------------- @@ -87,7 +87,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL event UpdatedTreasury(address newTreasury); event BnftHolderDeregistered(address user, uint256 index); event BnftHolderRegistered(address user, uint256 index); - event UpdatedSchedulingPeriod(uint128 newPeriodInSeconds); event ValidatorRegistered(uint256 indexed validatorId, bytes signature, bytes pubKey, bytes32 depositRoot); event ValidatorApproved(uint256 indexed validatorId); event ValidatorRegistrationCanceled(uint256 indexed validatorId); @@ -97,10 +96,8 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL error IncorrectCaller(); error InvalidAmount(); - error InvalidParams(); error DataNotSet(); error InsufficientLiquidity(); - error SendFail(); //-------------------------------------------------------------------------------------- //---------------------------- STATE-CHANGING FUNCTIONS ------------------------------ @@ -215,7 +212,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL if (amount > type(uint96).max || amount == 0 || share == 0) revert InvalidAmount(); // transfer shares to WithdrawRequestNFT contract from this contract - eETH.transferFrom(msg.sender, address(withdrawRequestNFT), amount); + IERC20(address(eETH)).safeTransferFrom(msg.sender, address(withdrawRequestNFT), amount); uint256 requestId = withdrawRequestNFT.requestWithdraw(uint96(amount), uint96(share), recipient, 0); @@ -251,7 +248,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL if (amount > type(uint96).max || amount == 0 || share == 0) revert InvalidAmount(); // transfer shares to WithdrawRequestNFT contract - eETH.transferFrom(msg.sender, address(withdrawRequestNFT), amount); + IERC20(address(eETH)).safeTransferFrom(msg.sender, address(withdrawRequestNFT), amount); uint256 requestId = withdrawRequestNFT.requestWithdraw(uint96(amount), uint96(share), recipient, fee); @@ -538,7 +535,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function burnEEthShares(uint256 shares) external { - if (msg.sender != address(etherFiRedemptionManager) || msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); + if (msg.sender != address(etherFiRedemptionManager) && msg.sender != address(withdrawRequestNFT)) revert IncorrectCaller(); eETH.burnShares(msg.sender, shares); } @@ -571,9 +568,9 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL } function _sendFund(address _recipient, uint256 _amount) internal { - uint256 balanace = address(this).balance; + uint256 balance = address(this).balance; (bool sent, ) = _recipient.call{value: _amount}(""); - require(sent && address(this).balance == balanace - _amount, "SendFail"); + require(sent && address(this).balance >= balance - _amount, "SendFail"); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} diff --git a/src/WithdrawRequestNFT.sol b/src/WithdrawRequestNFT.sol index dd7262079..7ad5f36c5 100644 --- a/src/WithdrawRequestNFT.sol +++ b/src/WithdrawRequestNFT.sol @@ -11,10 +11,13 @@ import "./interfaces/IWithdrawRequestNFT.sol"; import "./interfaces/IMembershipManager.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgradeable, IWithdrawRequestNFT { using Math for uint256; + using SafeERC20 for IERC20; uint256 private constant BASIS_POINT_SCALE = 1e4; address public immutable treasury; @@ -97,7 +100,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// @param recipient address to recieve with WithdrawRequestNFT /// @param fee fee to be subtracted from amount when recipient calls claimWithdraw /// @return uint256 id of the withdraw request - function requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) external payable onlyLiquidtyPool whenNotPaused returns (uint256) { + function requestWithdraw(uint96 amountOfEEth, uint96 shareOfEEth, address recipient, uint256 fee) external payable onlyLiquidityPool whenNotPaused returns (uint256) { uint256 requestId = nextRequestId++; uint32 feeGwei = uint32(fee / 1 gwei); @@ -253,6 +256,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad /// - Burn: the rest of the remainder is burned /// @param _eEthAmount: the remainder of the eEth amount function handleRemainder(uint256 _eEthAmount) external onlyAdmin { + require(_eEthAmount != 0, "EETH amount cannot be 0"); require(isScanOfShareRemainderCompleted(), "Not all prev requests have been scanned"); require(getEEthRemainderAmount() >= _eEthAmount, "Not enough eETH remainder"); @@ -265,8 +269,8 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad totalRemainderEEthShares -= eEthSharesToMoved; - eETH.transfer(treasury, eEthAmountToTreasury); - liquidityPool.burnEEthShares(eEthSharesToBurn); + if (eEthAmountToTreasury > 0) IERC20(address(eETH)).safeTransfer(treasury, eEthAmountToTreasury); + if (eEthSharesToBurn > 0) liquidityPool.burnEEthShares(eEthSharesToBurn); require (beforeEEthShares - eEthSharesToMoved == eETH.shares(address(this)), "Invalid eETH shares after remainder handling"); @@ -311,7 +315,7 @@ contract WithdrawRequestNFT is ERC721Upgradeable, UUPSUpgradeable, OwnableUpgrad _; } - modifier onlyLiquidtyPool() { + modifier onlyLiquidityPool() { require(msg.sender == address(liquidityPool), "Caller is not the liquidity pool"); _; } From c782ab3eb2309993982cd26c00b3bc481dd4fe23 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Mon, 20 Jan 2025 16:49:24 -0500 Subject: [PATCH 87/95] changed gas limit in redemption to 50k from 10k --- src/EtherFiRedemptionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 9584549cb..b0189d237 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -151,7 +151,7 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); // To Receiver by transferring ETH - (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); + (bool success, ) = receiver.call{value: ethReceived, gas: 50_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); // Make sure the liquidity pool balance is correct && total shares are correct From b877f7e606a3281871758fe2e001ea50f16d1dc3 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Mon, 20 Jan 2025 17:06:28 -0500 Subject: [PATCH 88/95] added only liquidity pool to burn shares --- src/EETH.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EETH.sol b/src/EETH.sol index 924b459de..9d8574122 100644 --- a/src/EETH.sol +++ b/src/EETH.sol @@ -68,7 +68,7 @@ contract EETH is IERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable, IERC20P } function burnShares(address _user, uint256 _share) external { - require(msg.sender == address(liquidityPool) || msg.sender == _user, "Incorrect Caller"); + require(msg.sender == address(liquidityPool), "Incorrect Caller"); require(shares[_user] >= _share, "BURN_AMOUNT_EXCEEDS_BALANCE"); shares[_user] -= _share; totalShares -= _share; From 3128cf3d83e13f802d0070cb3216179bd190bc6f Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Tue, 21 Jan 2025 10:56:22 -0500 Subject: [PATCH 89/95] remove batch cancel deposit by admin since not required --- src/EtherFiRedemptionManager.sol | 4 ++-- src/LiquidityPool.sol | 4 ---- test/LiquidityPool.t.sol | 10 +++++----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index b0189d237..da1229140 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -150,8 +150,8 @@ contract EtherFiRedemptionManager is Initializable, OwnableUpgradeable, Pausable // uint256 totalShares = eEth.totalShares(); require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); - // To Receiver by transferring ETH - (bool success, ) = receiver.call{value: ethReceived, gas: 50_000}(""); + // To Receiver by transferring ETH, using gas 10k for additional safety + (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); require(success, "EtherFiRedemptionManager: Transfer failed"); // Make sure the liquidity pool balance is correct && total shares are correct diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 58ebe0543..b15a96946 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -392,10 +392,6 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL _batchCancelDeposit(_validatorIds, msg.sender); } - function batchCancelDepositByAdmin(uint256[] calldata _validatorIds, address _bnftStaker) external whenNotPaused onlyAdmin { - _batchCancelDeposit(_validatorIds, _bnftStaker); - } - function _batchCancelDeposit(uint256[] calldata _validatorIds, address _bnftStaker) internal { uint256 returnAmount = 0; diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 815a7122a..7e64badbd 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -1304,12 +1304,12 @@ contract LiquidityPoolTest is TestSetup { vm.prank(owner); liquidityPoolInstance.updateAdmin(chad, true); - vm.prank(bob); - vm.expectRevert("Not admin"); - liquidityPoolInstance.batchCancelDepositByAdmin(bidIds, alice); + // vm.prank(bob); + // vm.expectRevert("Not admin"); + // liquidityPoolInstance.batchCancelDepositByAdmin(bidIds, alice); - vm.prank(chad); - liquidityPoolInstance.batchCancelDepositByAdmin(bidIds, alice); + // vm.prank(chad); + // liquidityPoolInstance.batchCancelDepositByAdmin(bidIds, alice); } function test_deopsitToRecipient_by_rando_fails() public { From ce827c209ee1979483801c58d1ecffe5b6c0d2a4 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Thu, 23 Jan 2025 10:24:29 -0500 Subject: [PATCH 90/95] removed duplicate bytecode_hash config from foundry.toml and added audit report --- ... - EtherFi - Withdrawal Fee - Re-audit.pdf | Bin 0 -> 627384 bytes foundry.toml | 1 - script/specialized/weEth_withdrawal_v2.s.sol | 30 ++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf diff --git a/audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf b/audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c179a8d8f7fc6e640d8b5d2a3366f2ccb217f401 GIT binary patch literal 627384 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 Date: Fri, 24 Jan 2025 13:43:19 -0500 Subject: [PATCH 91/95] instant withdrawal upgrade prepared --- ...27_upgrade_instant_withdrawal_execute.json | 11 +++ ...7_upgrade_instant_withdrawal_schedule.json | 11 +++ script/GnosisHelpers.sol | 84 +++++++++++++++++++ script/specialized/weEth_withdrawal_v2.s.sol | 66 +++++++++++---- 4 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 operations/20250127_upgrade_instant_withdrawal_execute.json create mode 100644 operations/20250127_upgrade_instant_withdrawal_schedule.json create mode 100644 script/GnosisHelpers.sol diff --git a/operations/20250127_upgrade_instant_withdrawal_execute.json b/operations/20250127_upgrade_instant_withdrawal_execute.json new file mode 100644 index 000000000..20be63d39 --- /dev/null +++ b/operations/20250127_upgrade_instant_withdrawal_execute.json @@ -0,0 +1,11 @@ +{ + "chainId": "1", + "meta": { "txBuilderVersion": "1.16.5" }, + "transactions": [ + { + "to": "0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", + "value": "0", + "data": "0xe38335e500000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000daee8abc6cde3070edd545a21c0a29462a6b4c556b95a6ba8d37d6bcf446251800000000000000000000000000000000000000000000000000000000000000020000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c000000000000000000000000308861a430be4cce5502d0a12724771fc6daf21600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000c44f1ef286000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044f8a025b40000000000000000000000009af1298993dc1f397973c62a5d47a284cf76844d0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a44f1ef2860000000000000000000000005991a2df15a8f6a256d3ec51e99254cd3fb576a900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024984856a30000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + ] +} diff --git a/operations/20250127_upgrade_instant_withdrawal_schedule.json b/operations/20250127_upgrade_instant_withdrawal_schedule.json new file mode 100644 index 000000000..de24243c8 --- /dev/null +++ b/operations/20250127_upgrade_instant_withdrawal_schedule.json @@ -0,0 +1,11 @@ +{ + "chainId": "1", + "meta": { "txBuilderVersion": "1.16.5" }, + "transactions": [ + { + "to": "0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", + "value": "0", + "data": "0x8f2a0bb000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000daee8abc6cde3070edd545a21c0a29462a6b4c556b95a6ba8d37d6bcf4462518000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c000000000000000000000000308861a430be4cce5502d0a12724771fc6daf21600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000c44f1ef286000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044f8a025b40000000000000000000000009af1298993dc1f397973c62a5d47a284cf76844d0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a44f1ef2860000000000000000000000005991a2df15a8f6a256d3ec51e99254cd3fb576a900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024984856a30000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + ] +} diff --git a/script/GnosisHelpers.sol b/script/GnosisHelpers.sol new file mode 100644 index 000000000..76ea6f3e7 --- /dev/null +++ b/script/GnosisHelpers.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "@openzeppelin/contracts/utils/Strings.sol"; + + +contract GnosisHelpers is Test { + + /** + * @dev Simulations the execution of a gnosis transaction bundle on the current fork + * @param transactionPath The path to the transaction bundle json file + * @param sender The address of the gnosis safe that will execute the transaction + */ + function executeGnosisTransactionBundle(string memory transactionPath, address sender) public { + string memory json = vm.readFile(transactionPath); + for (uint256 i = 0; vm.keyExistsJson(json, string.concat(".transactions[", Strings.toString(i), "]")); i++) { + address to = vm.parseJsonAddress(json, string.concat(string.concat(".transactions[", Strings.toString(i)), "].to")); + uint256 value = vm.parseJsonUint(json, string.concat(string.concat(".transactions[", Strings.toString(i)), "].value")); + bytes memory data = vm.parseJsonBytes(json, string.concat(string.concat(".transactions[", Strings.toString(i)), "].data")); + + vm.prank(sender); + (bool success,) = address(to).call{value: value}(data); + require(success, "Transaction failed"); + } + } + + // Get the gnosis transaction header + function _getGnosisHeader(string memory chainId) internal pure returns (string memory) { + return string.concat('{"chainId":"', chainId, '","meta": { "txBuilderVersion": "1.16.5" }, "transactions": ['); + } + + // Create a gnosis transaction + // ether sent value is always 0 for our usecase + function _getGnosisTransaction(string memory to, string memory data, bool isLast) internal pure returns (string memory) { + string memory suffix = isLast ? ']}' : ','; + return string.concat('{"to":"', to, '","value":"0","data":"', data, '"}', suffix); + } + + // Helper function to convert bytes to hex strings + // soldity encodes returns a bytes object and this must be converted to a hex string to be used in gnosis transactions + function iToHex(bytes memory buffer) public pure returns (string memory) { + // Fixed buffer size for hexadecimal convertion + bytes memory converted = new bytes(buffer.length * 2); + + bytes memory _base = "0123456789abcdef"; + + for (uint256 i = 0; i < buffer.length; i++) { + converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; + converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; + } + + return string(abi.encodePacked("0x", converted)); + } + + // Helper function to convert an address to a hex string of the bytes + function addressToHex(address addr) public pure returns (string memory) { + return iToHex(abi.encodePacked(addr)); + } + + address public timelock = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; + bytes32 public predecessor = 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 public salt = 0x0000000000000000000000000000000000000000000000000000000000000000; + uint256 public delay = 259200; + + // Generates the schedule transaction for a gnosis safe + function _getGnosisScheduleTransaction(address to, bytes memory data, bool isLasts) internal view returns (string memory) { + + string memory timelockAddressHex = iToHex(abi.encodePacked(address(timelock))); + string memory scheduleTransactionData = iToHex(abi.encodeWithSignature("schedule(address,uint256,bytes,bytes32,bytes32,uint256)", to, 0, data, predecessor, salt, delay)); + + return _getGnosisTransaction(timelockAddressHex, scheduleTransactionData, isLasts); + } + + function _getGnosisExecuteTransaction(address to, bytes memory data, bool isLasts) internal view returns (string memory) { + + string memory timelockAddressHex = iToHex(abi.encodePacked(address(timelock))); + string memory executeTransactionData = iToHex(abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", to, 0, data, predecessor, salt)); + + return _getGnosisTransaction(timelockAddressHex, executeTransactionData, isLasts); + } + +} \ No newline at end of file diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol index d8967c348..16cb67be8 100644 --- a/script/specialized/weEth_withdrawal_v2.s.sol +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -5,9 +5,11 @@ import "forge-std/Script.sol"; import "test/TestSetup.sol"; import "src/helpers/AddressProvider.sol"; +import "../GnosisHelpers.sol"; -contract Upgrade is Script { +contract Upgrade is Script, GnosisHelpers { + address public etherFiTimelock = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; address public addressProviderAddress = 0x8487c5F8550E3C3e7734Fe7DCF77DB2B72E4A848; AddressProvider public addressProvider = AddressProvider(addressProviderAddress); address public roleRegistry = 0x1d3Af47C1607A2EF33033693A9989D1d1013BB50; @@ -16,6 +18,7 @@ contract Upgrade is Script { WithdrawRequestNFT withdrawRequestNFTInstance; LiquidityPool liquidityPoolInstance; + EtherFiRedemptionManager etherFiRedemptionManagerInstance; function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); @@ -44,21 +47,45 @@ contract Upgrade is Script { 10_00, 1_00, 1_00, 5 ether, 0.001 ether // 10% fee split to treasury, 1% exit fee, 1% low watermark ) ); - EtherFiRedemptionManager etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); - // etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); - withdrawRequestNFTInstance.upgradeToAndCall( - address(new WithdrawRequestNFT(treasury)), + address withdrawRequestNFTImpl = address(new WithdrawRequestNFT(treasury)); + address liquidityPoolImpl = address(new LiquidityPool()); + + predecessor = 0x0000000000000000000000000000000000000000000000000000000000000000; + salt = keccak256(abi.encodePacked(address(withdrawRequestNFTInstance), address(liquidityPoolInstance), block.number)); + delay = 3 days; + + address[] memory targets = new address[](2); + targets[0] = address(withdrawRequestNFTInstance); + targets[1] = address(liquidityPoolInstance); + + uint256[] memory values = new uint256[](2); + + bytes[] memory payloads = new bytes[](2); + bytes memory upgradeWithdrawRequestNFTUpgradeData = abi.encodeWithSignature( + "upgradeToAndCall(address,bytes)", + withdrawRequestNFTImpl, abi.encodeWithSelector(WithdrawRequestNFT.initializeOnUpgrade.selector, pauser, 50_00) // 50% fee split to treasury ); - // withdrawRequestNFTInstance.initializeOnUpgrade(pauser, 50_00); // 50% fee split to treasury + bytes memory upgradeLiquidityPoolUpgradeData = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", liquidityPoolImpl, abi.encodeWithSelector(LiquidityPool.initializeOnUpgradeWithRedemptionManager.selector, address(etherFiRedemptionManagerInstance))); - liquidityPoolInstance.upgradeToAndCall(address(new LiquidityPool()), abi.encodeWithSelector(LiquidityPool.initializeOnUpgradeWithRedemptionManager.selector, address(etherFiRedemptionManagerInstance))); - // liquidityPoolInstance.initializeOnUpgradeWithRedemptionManager(address(etherFiRedemptionManagerInstance)); + payloads[0] = upgradeWithdrawRequestNFTUpgradeData; + payloads[1] = upgradeLiquidityPoolUpgradeData; - // verification - assert(withdrawRequestNFTInstance.pauser() == pauser); - assert(address(liquidityPoolInstance.etherFiRedemptionManager()) == address(etherFiRedemptionManagerInstance)); + string memory scheduleGnosisTx = _getGnosisHeader("1"); + string memory scheduleUpgrade = iToHex(abi.encodeWithSignature("scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)", targets, values, payloads, predecessor, salt, delay)); + scheduleGnosisTx = string(abi.encodePacked(scheduleGnosisTx, _getGnosisTransaction(addressToHex(timelock), scheduleUpgrade, true))); + + string memory path = "./operations/20250127_upgrade_instant_withdrawal_schedule.json"; + vm.writeFile(path, scheduleGnosisTx); + + string memory executeGnosisTx = _getGnosisHeader("1"); + string memory executeUpgrade = iToHex(abi.encodeWithSignature("executeBatch(address[],uint256[],bytes[],bytes32,bytes32)", targets, values, payloads, predecessor, salt)); + executeGnosisTx = string(abi.encodePacked(executeGnosisTx, _getGnosisTransaction(addressToHex(timelock), executeUpgrade, true))); + + path = "./operations/20250127_upgrade_instant_withdrawal_execute.json"; + vm.writeFile(path, executeGnosisTx); } function agg() internal { @@ -80,7 +107,6 @@ contract Upgrade is Script { } contract TestUpgrade is Test, Upgrade { - address etherFiTimelock = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; function setUp() public { vm.selectFork(vm.createFork(vm.envString("MAINNET_RPC_URL"))); @@ -89,9 +115,19 @@ contract TestUpgrade is Test, Upgrade { } function test_UpgradeWeETHInstantWithdrawal() public { - startHoax(etherFiTimelock); + // startHoax(etherFiTimelock); deploy_upgrade(); - agg(); - handle_remainder(); + // agg(); + // handle_remainder(); + + string memory path = "./operations/20250127_upgrade_instant_withdrawal_schedule.json"; + executeGnosisTransactionBundle(path, 0xcdd57D11476c22d265722F68390b036f3DA48c21); + + path = "./operations/20250127_upgrade_instant_withdrawal_execute.json"; + vm.warp(block.timestamp + 3 days + 1); + executeGnosisTransactionBundle(path, 0xcdd57D11476c22d265722F68390b036f3DA48c21); + + assert(withdrawRequestNFTInstance.pauser() == pauser); + assert(address(liquidityPoolInstance.etherFiRedemptionManager()) == address(etherFiRedemptionManagerInstance)); } } From 07bbcfadf064dfdec9e18ad952e7d10eea7dd1c7 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Tue, 28 Jan 2025 10:25:22 -0500 Subject: [PATCH 92/95] instant withdrawal script --- foundry.toml | 2 +- lib/solady | 1 + script/specialized/weEth_withdrawal_v2.s.sol | 21 ++++++++++---------- 3 files changed, 13 insertions(+), 11 deletions(-) create mode 160000 lib/solady diff --git a/foundry.toml b/foundry.toml index 0a00b79a8..bd4601c3e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ src = 'src' out = 'out' libs = ['lib'] -fs_permissions = [{ access = "read-write", path = "./release"}, { access = "read", path = "./test" }] +fs_permissions = [{ access = "read-write", path = "./release"}, { access = "read", path = "./test" }, { access = "read-write", path = "./operations" }] gas_reports = ["*"] optimizer_runs = 2000 extra_output = ["storageLayout"] diff --git a/lib/solady b/lib/solady new file mode 160000 index 000000000..8583a6e38 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 8583a6e386b897f3db142a541f86d5953eccd835 diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol index 16cb67be8..4a8313008 100644 --- a/script/specialized/weEth_withdrawal_v2.s.sol +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -29,8 +29,8 @@ contract Upgrade is Script, GnosisHelpers { vm.startBroadcast(deployerPrivateKey); deploy_upgrade(); - agg(); - handle_remainder(); + // agg(); + // handle_remainder(); vm.stopBroadcast(); } @@ -42,12 +42,14 @@ contract Upgrade is Script, GnosisHelpers { addressProvider.getContractAddress("WeETH"), treasury, roleRegistry)), - abi.encodeWithSelector( - EtherFiRedemptionManager.initialize.selector, - 10_00, 1_00, 1_00, 5 ether, 0.001 ether // 10% fee split to treasury, 1% exit fee, 1% low watermark - ) + "" ); etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); + etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 2_30, 500 ether, 0.005787037037 ether); + // abi.encodeWithSelector( + // EtherFiRedemptionManager.initialize.selector, + // 10_00, 1_00, 2_30, 500 ether, 0.005787037037 ether // 10% fee split to treasury, 1% exit fee, 1% low watermark + // ) address withdrawRequestNFTImpl = address(new WithdrawRequestNFT(treasury)); address liquidityPoolImpl = address(new LiquidityPool()); @@ -64,12 +66,11 @@ contract Upgrade is Script, GnosisHelpers { bytes[] memory payloads = new bytes[](2); bytes memory upgradeWithdrawRequestNFTUpgradeData = abi.encodeWithSignature( - "upgradeToAndCall(address,bytes)", + "upgradeToAndCall(address,bytes)", withdrawRequestNFTImpl, abi.encodeWithSelector(WithdrawRequestNFT.initializeOnUpgrade.selector, pauser, 50_00) // 50% fee split to treasury ); bytes memory upgradeLiquidityPoolUpgradeData = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", liquidityPoolImpl, abi.encodeWithSelector(LiquidityPool.initializeOnUpgradeWithRedemptionManager.selector, address(etherFiRedemptionManagerInstance))); - payloads[0] = upgradeWithdrawRequestNFTUpgradeData; payloads[1] = upgradeLiquidityPoolUpgradeData; @@ -77,14 +78,14 @@ contract Upgrade is Script, GnosisHelpers { string memory scheduleUpgrade = iToHex(abi.encodeWithSignature("scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)", targets, values, payloads, predecessor, salt, delay)); scheduleGnosisTx = string(abi.encodePacked(scheduleGnosisTx, _getGnosisTransaction(addressToHex(timelock), scheduleUpgrade, true))); - string memory path = "./operations/20250127_upgrade_instant_withdrawal_schedule.json"; + string memory path = "./operations/20250128_upgrade_instant_withdrawal_schedule.json"; vm.writeFile(path, scheduleGnosisTx); string memory executeGnosisTx = _getGnosisHeader("1"); string memory executeUpgrade = iToHex(abi.encodeWithSignature("executeBatch(address[],uint256[],bytes[],bytes32,bytes32)", targets, values, payloads, predecessor, salt)); executeGnosisTx = string(abi.encodePacked(executeGnosisTx, _getGnosisTransaction(addressToHex(timelock), executeUpgrade, true))); - path = "./operations/20250127_upgrade_instant_withdrawal_execute.json"; + path = "./operations/20250128_upgrade_instant_withdrawal_execute.json"; vm.writeFile(path, executeGnosisTx); } From 3b4be39801689c9c840403268eb5506de0d92683 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Tue, 28 Jan 2025 11:48:38 -0500 Subject: [PATCH 93/95] deployed instant withdrawal upgrade --- ....json => 20250128_upgrade_instant_withdrawal_execute.json} | 2 +- ...json => 20250128_upgrade_instant_withdrawal_schedule.json} | 2 +- script/specialized/weEth_withdrawal_v2.s.sol | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename operations/{20250127_upgrade_instant_withdrawal_execute.json => 20250128_upgrade_instant_withdrawal_execute.json} (78%) rename operations/{20250127_upgrade_instant_withdrawal_schedule.json => 20250128_upgrade_instant_withdrawal_schedule.json} (78%) diff --git a/operations/20250127_upgrade_instant_withdrawal_execute.json b/operations/20250128_upgrade_instant_withdrawal_execute.json similarity index 78% rename from operations/20250127_upgrade_instant_withdrawal_execute.json rename to operations/20250128_upgrade_instant_withdrawal_execute.json index 20be63d39..7a07c47bc 100644 --- a/operations/20250127_upgrade_instant_withdrawal_execute.json +++ b/operations/20250128_upgrade_instant_withdrawal_execute.json @@ -5,7 +5,7 @@ { "to": "0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", "value": "0", - "data": "0xe38335e500000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000daee8abc6cde3070edd545a21c0a29462a6b4c556b95a6ba8d37d6bcf446251800000000000000000000000000000000000000000000000000000000000000020000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c000000000000000000000000308861a430be4cce5502d0a12724771fc6daf21600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000c44f1ef286000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044f8a025b40000000000000000000000009af1298993dc1f397973c62a5d47a284cf76844d0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a44f1ef2860000000000000000000000005991a2df15a8f6a256d3ec51e99254cd3fb576a900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024984856a30000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "data": "0xe38335e500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000012cd17fdc855070e6b92a85ee62b7b5f78b031a423542fabd5d0ace52bdd8aea00000000000000000000000000000000000000000000000000000000000000020000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c000000000000000000000000308861a430be4cce5502d0a12724771fc6daf21600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000c44f1ef28600000000000000000000000078f424c42f006b046b927d49b6d7abef74ca67ae00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044f8a025b40000000000000000000000009af1298993dc1f397973c62a5d47a284cf76844d0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a44f1ef286000000000000000000000000a05d566bac16e2fbcec4cff68e89119ee2a96ef900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024984856a3000000000000000000000000d5fd46f4df70a63d60a8563cad0444fcc25dce7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" } ] } diff --git a/operations/20250127_upgrade_instant_withdrawal_schedule.json b/operations/20250128_upgrade_instant_withdrawal_schedule.json similarity index 78% rename from operations/20250127_upgrade_instant_withdrawal_schedule.json rename to operations/20250128_upgrade_instant_withdrawal_schedule.json index de24243c8..f1c3eaae6 100644 --- a/operations/20250127_upgrade_instant_withdrawal_schedule.json +++ b/operations/20250128_upgrade_instant_withdrawal_schedule.json @@ -5,7 +5,7 @@ { "to": "0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", "value": "0", - "data": "0x8f2a0bb000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000daee8abc6cde3070edd545a21c0a29462a6b4c556b95a6ba8d37d6bcf4462518000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c000000000000000000000000308861a430be4cce5502d0a12724771fc6daf21600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000c44f1ef286000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044f8a025b40000000000000000000000009af1298993dc1f397973c62a5d47a284cf76844d0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a44f1ef2860000000000000000000000005991a2df15a8f6a256d3ec51e99254cd3fb576a900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024984856a30000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "data": "0x8f2a0bb000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000012cd17fdc855070e6b92a85ee62b7b5f78b031a423542fabd5d0ace52bdd8aea000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c000000000000000000000000308861a430be4cce5502d0a12724771fc6daf21600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000c44f1ef28600000000000000000000000078f424c42f006b046b927d49b6d7abef74ca67ae00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044f8a025b40000000000000000000000009af1298993dc1f397973c62a5d47a284cf76844d0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a44f1ef286000000000000000000000000a05d566bac16e2fbcec4cff68e89119ee2a96ef900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024984856a3000000000000000000000000d5fd46f4df70a63d60a8563cad0444fcc25dce7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" } ] } diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol index 4a8313008..df61468f0 100644 --- a/script/specialized/weEth_withdrawal_v2.s.sol +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -21,12 +21,12 @@ contract Upgrade is Script, GnosisHelpers { EtherFiRedemptionManager etherFiRedemptionManagerInstance; function run() external { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + // uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); withdrawRequestNFTInstance = WithdrawRequestNFT(payable(addressProvider.getContractAddress("WithdrawRequestNFT"))); liquidityPoolInstance = LiquidityPool(payable(addressProvider.getContractAddress("LiquidityPool"))); - vm.startBroadcast(deployerPrivateKey); + vm.startBroadcast(); deploy_upgrade(); // agg(); From c2e6a53aea45865818ab2f275ef1e7c86be0af5d Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Tue, 28 Jan 2025 11:58:21 -0500 Subject: [PATCH 94/95] fix test --- script/specialized/weEth_withdrawal_v2.s.sol | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/script/specialized/weEth_withdrawal_v2.s.sol b/script/specialized/weEth_withdrawal_v2.s.sol index df61468f0..2b9cc4832 100644 --- a/script/specialized/weEth_withdrawal_v2.s.sol +++ b/script/specialized/weEth_withdrawal_v2.s.sol @@ -112,20 +112,17 @@ contract TestUpgrade is Test, Upgrade { vm.selectFork(vm.createFork(vm.envString("MAINNET_RPC_URL"))); withdrawRequestNFTInstance = WithdrawRequestNFT(payable(addressProvider.getContractAddress("WithdrawRequestNFT"))); - liquidityPoolInstance = LiquidityPool(payable(addressProvider.getContractAddress("LiquidityPool"))); + liquidityPoolInstance = LiquidityPool(payable(addressProvider.getContractAddress("LiquidityPool"))); + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(0xD5fd46F4df70a63d60a8563CaD0444Fcc25dcE7f)); } function test_UpgradeWeETHInstantWithdrawal() public { - // startHoax(etherFiTimelock); - deploy_upgrade(); - // agg(); - // handle_remainder(); - - string memory path = "./operations/20250127_upgrade_instant_withdrawal_schedule.json"; + string memory path = "./operations/20250128_upgrade_instant_withdrawal_schedule.json"; executeGnosisTransactionBundle(path, 0xcdd57D11476c22d265722F68390b036f3DA48c21); - - path = "./operations/20250127_upgrade_instant_withdrawal_execute.json"; + vm.warp(block.timestamp + 3 days + 1); + + path = "./operations/20250128_upgrade_instant_withdrawal_execute.json"; executeGnosisTransactionBundle(path, 0xcdd57D11476c22d265722F68390b036f3DA48c21); assert(withdrawRequestNFTInstance.pauser() == pauser); From 95cfaaaddbaf3a23d8d8f08916c8705fe640caf9 Mon Sep 17 00:00:00 2001 From: syko Date: Fri, 7 Feb 2025 01:36:46 +0900 Subject: [PATCH 95/95] Rename 2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf to 2025.01.23 - Certora - EtherFi - Withdrawal Fee.pdf Signed-off-by: syko --- ....01.23 - Certora - EtherFi - Withdrawal Fee.pdf} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename audits/{2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf => 2025.01.23 - Certora - EtherFi - Withdrawal Fee.pdf} (100%) diff --git a/audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf b/audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee.pdf similarity index 100% rename from audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf rename to audits/2025.01.23 - Certora - EtherFi - Withdrawal Fee.pdf

u^7fxMGuO-I7F8o=_vActXyc)_NqtRBKo;; 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%0Dj|L?DB(6*a`T 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