Skip to content

Commit

Permalink
implemented instant fee mechanism, handling of the implicit fee
Browse files Browse the repository at this point in the history
  • Loading branch information
seongyun-ko committed Dec 9, 2024
1 parent d356cc9 commit 82fab2e
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 55 deletions.
89 changes: 57 additions & 32 deletions src/EtherFiWithdrawalBuffer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,41 @@ 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);

_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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}

/**
Expand All @@ -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 {}
Expand Down
79 changes: 64 additions & 15 deletions src/WithdrawRequestNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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`:,
Expand Down Expand Up @@ -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++) {
Expand Down
Loading

0 comments on commit 82fab2e

Please sign in to comment.