Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

weETH withdrawal: Instant withdrawal with Fee + Implicit withdrawal fee handling #207

Open
wants to merge 99 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
d356cc9
init. Instant Withdrawal via Buffer
seongyun-ko Dec 6, 2024
82fab2e
implemented instant fee mechanism, handling of the implicit fee
seongyun-ko Dec 9, 2024
0f13953
fix scripts
seongyun-ko Dec 12, 2024
0b68310
add role registry, consider eth amount locked for withdrawal in liqui…
seongyun-ko Dec 17, 2024
c9fd604
use setter for 'shareRemainderSplitToTreasuryInBps', add more fuzz te…
seongyun-ko Dec 19, 2024
c377e8e
handle issues in calculating the dust shares
seongyun-ko Dec 20, 2024
a3aeac9
improve comments
seongyun-ko Dec 20, 2024
57bd0fc
add sorted & unique constraints + type change to reduce gas
seongyun-ko Dec 24, 2024
b6100b6
update 'handleAccumulatedShareRemainder' to be callable by admin
seongyun-ko Dec 24, 2024
e8734d6
Certora audit: (1) add {aggregateSumEEthShareAmount}, (2) fix {_claim…
seongyun-ko Dec 26, 2024
71ffa8d
wip: to be amended
seongyun-ko Dec 30, 2024
9e0fb99
add simplified {invalidate, validate} request, fix unit tests
seongyun-ko Dec 30, 2024
98f483a
rename EtherFiWithdrawBuffer -> EtherFiRedemptionManager
seongyun-ko Dec 30, 2024
bcc1184
fix the logic to check the aggr calls
seongyun-ko Dec 30, 2024
1f85fb6
Update test/WithdrawRequestNFT.t.sol
jtfirek Dec 30, 2024
bdee463
reduce gas spending for 'call', update the upgrade init function, rem…
seongyun-ko Dec 30, 2024
d40a117
apply gas opt for BucketLimiter
seongyun-ko Dec 30, 2024
f4f2ffd
improve assetion tsets, apply design pattern, function rename
seongyun-ko Dec 30, 2024
5069c14
apply CEI pattern to 'handleRemainder'
seongyun-ko Dec 30, 2024
2e13202
apply CEI pattern to 'redeem'
seongyun-ko Dec 31, 2024
b18bd18
use 'totalRemainderEEthShares' instead of locked share
seongyun-ko Dec 31, 2024
1e12673
initializeOnUpgrade cant be called twice
seongyun-ko Dec 31, 2024
c14c348
initializeOnUpgrade onlyOnce
seongyun-ko Dec 31, 2024
35fba66
use uint256 instead of uint32
seongyun-ko Dec 31, 2024
e95cfc0
revert
seongyun-ko Dec 31, 2024
bbf2d83
improve the fuzz test
seongyun-ko Dec 31, 2024
2cbbc04
(1) pause the contract on upgrade, (2) prevent from calling 'aggregat…
seongyun-ko Jan 1, 2025
1482cb0
only owner of the funds can call {redeemEEth, redeemWeEth}
seongyun-ko Jan 2, 2025
8ced81e
disable unpause until the scan is completed
seongyun-ko Jan 2, 2025
86ac4a0
check the basis points params are below 1e4
seongyun-ko Jan 2, 2025
1e8cdea
disallow calling 'initializeOnUpgradeWithRedemptionManager' with inva…
seongyun-ko Jan 2, 2025
6fffba6
(1) withdrawRequestNFT cannot call LiquidityPool.addEthAmountLockedFo…
seongyun-ko Jan 2, 2025
89db9d5
prevent finalizing future requests
seongyun-ko Jan 2, 2025
c5a2dd1
add {redeemEEthWithPermit, redeemWeEthWithPermit}, use try-catch for
seongyun-ko Jan 2, 2025
c85e920
remove a redundant check
seongyun-ko Jan 2, 2025
cca361e
Prevent 'initializeOnUpgrade' of LiquidityPool from being called twic…
seongyun-ko Jan 2, 2025
268efe5
use 'isScanOfShareRemainderCompleted'
seongyun-ko Jan 2, 2025
a13919d
add the max value constraint on rate limit
seongyun-ko Jan 2, 2025
58fe3fb
remove equality condition
seongyun-ko Jan 2, 2025
898896a
fix the overflow issue in '_refill'
seongyun-ko Jan 2, 2025
25ac914
Update EtherFiRedemptionManager.sol
seongyun-ko Jan 2, 2025
fd52a52
prevent the total shares from going below 1 gwei after redemption
seongyun-ko Jan 2, 2025
6eefec0
init. Instant Withdrawal via Buffer
seongyun-ko Dec 6, 2024
9963471
implemented instant fee mechanism, handling of the implicit fee
seongyun-ko Dec 9, 2024
9dcb732
fix scripts
seongyun-ko Dec 12, 2024
0e35c6b
add role registry, consider eth amount locked for withdrawal in liqui…
seongyun-ko Dec 17, 2024
77e61d8
use setter for 'shareRemainderSplitToTreasuryInBps', add more fuzz te…
seongyun-ko Dec 19, 2024
1435cc8
handle issues in calculating the dust shares
seongyun-ko Dec 20, 2024
8565d21
improve comments
seongyun-ko Dec 20, 2024
805bf07
add sorted & unique constraints + type change to reduce gas
seongyun-ko Dec 24, 2024
2de521a
update 'handleAccumulatedShareRemainder' to be callable by admin
seongyun-ko Dec 24, 2024
fc4a179
Certora audit: (1) add {aggregateSumEEthShareAmount}, (2) fix {_claim…
seongyun-ko Dec 26, 2024
8acd291
wip: to be amended
seongyun-ko Dec 30, 2024
a7e974b
add simplified {invalidate, validate} request, fix unit tests
seongyun-ko Dec 30, 2024
83bcd4f
rename EtherFiWithdrawBuffer -> EtherFiRedemptionManager
seongyun-ko Dec 30, 2024
34f67b9
fix the logic to check the aggr calls
seongyun-ko Dec 30, 2024
da5af14
Update test/WithdrawRequestNFT.t.sol
jtfirek Dec 30, 2024
577587b
reduce gas spending for 'call', update the upgrade init function, rem…
seongyun-ko Dec 30, 2024
980af7d
apply gas opt for BucketLimiter
seongyun-ko Dec 30, 2024
599d287
improve assetion tsets, apply design pattern, function rename
seongyun-ko Dec 30, 2024
5bd6630
apply CEI pattern to 'handleRemainder'
seongyun-ko Dec 30, 2024
2dd2115
apply CEI pattern to 'redeem'
seongyun-ko Dec 31, 2024
fbcd9f1
use 'totalRemainderEEthShares' instead of locked share
seongyun-ko Dec 31, 2024
3730663
initializeOnUpgrade cant be called twice
seongyun-ko Dec 31, 2024
72172e1
initializeOnUpgrade onlyOnce
seongyun-ko Dec 31, 2024
9dd6db1
use uint256 instead of uint32
seongyun-ko Dec 31, 2024
b61899b
revert
seongyun-ko Dec 31, 2024
80c21be
improve the fuzz test
seongyun-ko Dec 31, 2024
4862685
(1) pause the contract on upgrade, (2) prevent from calling 'aggregat…
seongyun-ko Jan 1, 2025
f36c180
only owner of the funds can call {redeemEEth, redeemWeEth}
seongyun-ko Jan 2, 2025
3356771
disable unpause until the scan is completed
seongyun-ko Jan 2, 2025
ddd1b59
check the basis points params are below 1e4
seongyun-ko Jan 2, 2025
43a5a5e
disallow calling 'initializeOnUpgradeWithRedemptionManager' with inva…
seongyun-ko Jan 2, 2025
aa53280
(1) withdrawRequestNFT cannot call LiquidityPool.addEthAmountLockedFo…
seongyun-ko Jan 2, 2025
8ae4844
prevent finalizing future requests
seongyun-ko Jan 2, 2025
26b5a0b
add {redeemEEthWithPermit, redeemWeEthWithPermit}, use try-catch for
seongyun-ko Jan 2, 2025
a785f25
remove a redundant check
seongyun-ko Jan 2, 2025
8785224
Prevent 'initializeOnUpgrade' of LiquidityPool from being called twic…
seongyun-ko Jan 2, 2025
edc30cb
use 'isScanOfShareRemainderCompleted'
seongyun-ko Jan 2, 2025
b666808
add the max value constraint on rate limit
seongyun-ko Jan 2, 2025
23fd48b
remove equality condition
seongyun-ko Jan 2, 2025
7a909ac
fix the overflow issue in '_refill'
seongyun-ko Jan 2, 2025
0e6662b
Update EtherFiRedemptionManager.sol
seongyun-ko Jan 2, 2025
f2cbc82
prevent the total shares from going below 1 gwei after redemption
seongyun-ko Jan 2, 2025
d193b00
fixes for Instant Withdrawal
shivam-ef Jan 16, 2025
17aabaa
fix: certora audit fixes
shivam-ef Jan 20, 2025
c782ab3
changed gas limit in redemption to 50k from 10k
shivam-ef Jan 20, 2025
b877f7e
added only liquidity pool to burn shares
shivam-ef Jan 20, 2025
3128cf3
remove batch cancel deposit by admin since not required
shivam-ef Jan 21, 2025
fdbf21a
Merge branch 'syko/feature/instant_withdrawal' of https://github.com/…
shivam-ef Jan 21, 2025
1cb9971
Merge branch 'master' of https://github.com/etherfi-protocol/smart-co…
shivam-ef Jan 22, 2025
fd0f75f
Merge branch 'syko/feature/instant_withdrawal' of https://github.com/…
shivam-ef Jan 22, 2025
ce827c2
removed duplicate bytecode_hash config from foundry.toml and added au…
shivam-ef Jan 23, 2025
3230fa7
instant withdrawal upgrade prepared
shivam-ef Jan 24, 2025
07bbcfa
instant withdrawal script
shivam-ef Jan 28, 2025
3b4be39
deployed instant withdrawal upgrade
shivam-ef Jan 28, 2025
c2e6a53
fix test
shivam-ef Jan 28, 2025
95cfaaa
Rename 2025.01.23 - Certora - EtherFi - Withdrawal Fee - Re-audit.pdf…
seongyun-ko Feb 6, 2025
824bcf8
Merge pull request #227 from etherfi-protocol/shivam/fix/instant_with…
shivam-ef Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/BucketLimiter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions script/deploys/DeployEtherFiRestaker.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
40 changes: 40 additions & 0 deletions script/deploys/DeployEtherFiWithdrawalBuffer.s.sol
Original file line number Diff line number Diff line change
@@ -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/EtherFiRedemptionManager.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);

EtherFiRedemptionManager impl = new EtherFiRedemptionManager(
addressProvider.getContractAddress("LiquidityPool"),
addressProvider.getContractAddress("EETH"),
addressProvider.getContractAddress("WeETH"),
0x0c83EAe1FE72c390A02E426572854931EefF93BA, // protocol safe
0x1d3Af47C1607A2EF33033693A9989D1d1013BB50 // role registry
);
UUPSProxy proxy = new UUPSProxy(payable(impl), "");

EtherFiRedemptionManager instance = EtherFiRedemptionManager(payable(proxy));
instance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether);

vm.stopBroadcast();
}
}
2 changes: 1 addition & 1 deletion script/deploys/DeployPhaseTwo.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ contract DeployPhaseTwoScript is Script {
}
retrieve_contract_addresses();

withdrawRequestNftImplementation = new WithdrawRequestNFT();
withdrawRequestNftImplementation = new WithdrawRequestNFT(address(0));
withdrawRequestNftProxy = new UUPSProxy(address(withdrawRequestNftImplementation), "");
withdrawRequestNftInstance = WithdrawRequestNFT(payable(withdrawRequestNftProxy));

Expand Down
2 changes: 1 addition & 1 deletion script/upgrades/WithdrawRequestNFTUpgradeScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));

oracleInstance.upgradeTo(address(v2Implementation));

Expand Down
280 changes: 280 additions & 0 deletions src/EtherFiRedemptionManager.sol
Original file line number Diff line number Diff line change
@@ -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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to do a require statement for clear error messages

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg

uint256 ethReceived = address(this).balance - prevBalance;

// To Stakers by burning shares
eEth.burnShares(address(this), feeShareToStakers);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to whitelist redemption manager as well for being able to call burn shares in liquidity pool with latest fix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess its better to just add a burnShares function on Liquidity pool which can be called by this contract instead, since LP is aware of this contract already while EETH would need to add another var to track this contract addr


// 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}("");
seongyun-ko marked this conversation as resolved.
Show resolved Hide resolved
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);
_;
}

}
Loading
Loading