From 0b68310623d61f3c8ebc481e5b3f90743e237f52 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Tue, 17 Dec 2024 14:43:25 +0900 Subject: [PATCH] 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 811d8d14..a79b9e94 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 2f12fd76..a71b2d6c 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 ce7f9954..d73268fd 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 423a3b61..9c9fe030 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));