Skip to content

Commit

Permalink
Merge pull request #201 from etherfi-protocol/merge-liquifier-changes
Browse files Browse the repository at this point in the history
Integrate Liquifier Changes
  • Loading branch information
jtfirek authored Nov 19, 2024
2 parents aff0349 + 6959be5 commit 2175e18
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 232 deletions.
142 changes: 40 additions & 102 deletions src/Liquifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
uint32 public DEPRECATED_eigenLayerWithdrawalClaimGasCost;
uint32 public DEPRECATED_timeBoundCapRefreshInterval; // seconds

bool public DEPRECATED_quoteStEthWithCurve;
bool public quoteStEthWithCurve;

uint128 public DEPRECATED_accumulatedFee;

Expand Down Expand Up @@ -89,6 +89,8 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab

mapping(address => bool) public DEPRECATED_pausers;

address etherFiRestakeManager;

RoleRegistry public roleRegistry;

BucketRateLimiter public rateLimiter;
Expand Down Expand Up @@ -148,6 +150,10 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
rateLimiter = BucketRateLimiter(_rateLimiter);
}

function initializeOnRestakerUpgrade(address _etherFiRestakeManager) external onlyOwner {
etherFiRestakeManager = _etherFiRestakeManager;
}

/// Deposit Liquid Staking Token such as stETH and Mint eETH
/// @param _token The address of the token to deposit
/// @param _amount The amount of the token to deposit
Expand All @@ -157,15 +163,16 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
function depositWithERC20(address _token, uint256 _amount, address _referral) public whenNotPaused nonReentrant returns (uint256) {
require(isTokenWhitelisted(_token) && (!tokenInfos[_token].isL2Eth || msg.sender == l1SyncPool), "NOT_ALLOWED");

IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
if (tokenInfos[_token].isL2Eth) {
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
} else {
IERC20(_token).safeTransferFrom(msg.sender, address(etherFiRestakeManager), _amount);
}

// The L1SyncPool's `_anticipatedDeposit` should be the only place to mint the `token` and always send its entirety to the Liquifier contract
if(tokenInfos[_token].isL2Eth) _L2SanityChecks(_token);

uint256 dx = quoteByMarketValue(_token, _amount);

// discount
dx = (10000 - tokenInfos[_token].discountInBasisPoints) * dx / 10000;
uint256 dx = quoteByDiscountedValue(_token, _amount);

uint256 eEthShare = liquidityPool.depositToRecipient(msg.sender, dx, _referral);

Expand Down Expand Up @@ -195,57 +202,17 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
return depositWithERC20(_token, _amount, _referral);
}

/// Initiate the process for redemption of stETH
function stEthRequestWithdrawal() external returns (uint256[] memory) {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();

uint256 amount = lido.balanceOf(address(this));
return stEthRequestWithdrawal(amount);
}

function stEthRequestWithdrawal(uint256 _amount) public returns (uint256[] memory) {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
if (_amount < lidoWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()) revert IncorrectAmount();
if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance();

tokenInfos[address(lido)].ethAmountPendingForWithdrawals += uint128(_amount);

uint256 maxAmount = lidoWithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT();
uint256 numReqs = (_amount + maxAmount - 1) / maxAmount;
uint256[] memory reqAmounts = new uint256[](numReqs);
for (uint256 i = 0; i < numReqs; i++) {
reqAmounts[i] = (i == numReqs - 1) ? _amount - i * maxAmount : maxAmount;
}
lido.approve(address(lidoWithdrawalQueue), _amount);
uint256[] memory reqIds = lidoWithdrawalQueue.requestWithdrawals(reqAmounts, address(this));

emit QueuedStEthWithdrawals(reqIds);

return reqIds;
}

/// @notice Claim a batch of withdrawal requests if they are finalized sending the ETH to the this contract back
/// @param _requestIds array of request ids to claim
/// @param _hints checkpoint hint for each id. Can be obtained with `findCheckpointHints()`
function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
uint256 balance = address(this).balance;
lidoWithdrawalQueue.claimWithdrawals(_requestIds, _hints);
uint256 newBalance = address(this).balance;

// to prevent the underflow error
uint128 dx = uint128(_min(newBalance - balance, tokenInfos[address(lido)].ethAmountPendingForWithdrawals));
tokenInfos[address(lido)].ethAmountPendingForWithdrawals -= dx;

emit CompletedStEthQueuedWithdrawals(_requestIds);
}

// Send the redeemed ETH back to the liquidity pool & Send the fee to Treasury
function withdrawEther() external {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
_withdrawEther();
}

function sendToEtherFiRestakeManager(address _token, uint256 _amount) external {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
IERC20(_token).safeTransfer(etherFiRestakeManager, _amount);
}

// Swap Liquifier's eETH for ETH from the liquidity pool and send it back to the liquidity pool
function withdrawEEth(uint256 amount) external {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
Expand All @@ -272,6 +239,16 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
tokenInfos[_token].totalCapInEther = 0;
}

function updateDiscountInBasisPoints(address _token, uint16 _discountInBasisPoints) external {
if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_PAUSER(), msg.sender)) revert IncorrectRole();
tokenInfos[_token].discountInBasisPoints = _discountInBasisPoints;
}

function updateQuoteStEthWithCurve(bool _quoteStEthWithCurve) external {
if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_PAUSER(), msg.sender)) revert IncorrectRole();
quoteStEthWithCurve = _quoteStEthWithCurve;
}

// Pauses the contract
function pauseContract() external {
if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_PAUSER(), msg.sender)) revert IncorrectRole();
Expand All @@ -294,56 +271,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
return msg.value;
}

// uint256 _amount, uint24 _fee, uint256 _minOutputAmount, uint256 _maxWaitingTime
function pancakeSwapForEth(address _token, uint256 _amount, uint24 _fee, uint256 _minOutputAmount, uint256 _maxWaitingTime) external {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
if (_amount > IERC20(_token).balanceOf(address(this))) revert NotEnoughBalance();
uint256 beforeBalance = address(this).balance;

IERC20(_token).approve(address(pancakeRouter), _amount);

IPancackeV3SwapRouter.ExactInputSingleParams memory input = IPancackeV3SwapRouter.ExactInputSingleParams({
tokenIn: _token,
tokenOut: pancakeRouter.WETH9(),
fee: _fee,
recipient: address(pancakeRouter),
deadline: block.timestamp + _maxWaitingTime,
amountIn: _amount,
amountOutMinimum: _minOutputAmount,
sqrtPriceLimitX96: 0
});
uint256 amountOut = pancakeRouter.exactInputSingle(input);

pancakeRouter.unwrapWETH9(amountOut, address(this));

uint256 currentBalance = address(this).balance;
if (currentBalance < _minOutputAmount + beforeBalance) revert WrongOutput();
}

function swapCbEthToEth(uint256 _amount, uint256 _minOutputAmount) external returns (uint256) {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
if (_amount > cbEth.balanceOf(address(this))) revert NotEnoughBalance();

cbEth.approve(address(cbEth_Eth_Pool), _amount);
return cbEth_Eth_Pool.exchange_underlying(1, 0, _amount, _minOutputAmount);
}

function swapWbEthToEth(uint256 _amount, uint256 _minOutputAmount) external returns (uint256) {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
if (_amount > wbEth.balanceOf(address(this))) revert NotEnoughBalance();

wbEth.approve(address(wbEth_Eth_Pool), _amount);
return wbEth_Eth_Pool.exchange(1, 0, _amount, _minOutputAmount);
}

function swapStEthToEth(uint256 _amount, uint256 _minOutputAmount) external returns (uint256) {
if (!roleRegistry.hasRole(LIQUIFIER_ADMIN_ROLE, msg.sender)) revert IncorrectRole();
if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance();

lido.approve(address(stEth_Eth_Pool), _amount);
return stEth_Eth_Pool.exchange(1, 0, _amount, _minOutputAmount);
}

/* VIEW FUNCTIONS */

// Given the `_amount` of `_token` token, returns the equivalent amount of ETH
Expand All @@ -362,7 +289,11 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
if (!isTokenWhitelisted(_token)) revert NotSupportedToken();

if (_token == address(lido)) {
return _amount; /// 1:1 from stETH to eETH
if (quoteStEthWithCurve) {
return _min(_amount, ICurvePoolQuoter1(address(stEth_Eth_Pool)).get_dy(1, 0, _amount));
} else {
return _amount; /// 1:1 from stETH to eETH
}
} else if (_token == address(cbEth)) {
return _min(_amount * cbEth.exchangeRate() / 1e18, ICurvePoolQuoter2(address(cbEth_Eth_Pool)).get_dy(1, 0, _amount));
} else if (_token == address(wbEth)) {
Expand All @@ -375,6 +306,13 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
revert NotSupportedToken();
}

// Calculates the amount of eETH that will be minted for a given token considering the discount rate
function quoteByDiscountedValue(address _token, uint256 _amount) public view returns (uint256) {
uint256 marketValue = quoteByMarketValue(_token, _amount);

return (10000 - tokenInfos[_token].discountInBasisPoints) * marketValue / 10000;
}

function isTokenWhitelisted(address _token) public view returns (bool) {
return tokenInfos[_token].isWhitelisted;
}
Expand Down
121 changes: 12 additions & 109 deletions test/Liquifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,25 @@ contract LiquifierTest is TestSetup {

vm.deal(alice, 100 ether);

vm.startPrank(liquifierInstance.owner());
liquifierInstance.updateQuoteStEthWithCurve(true);
liquifierInstance.updateDiscountInBasisPoints(address(stEth), 500); // 5%
vm.stopPrank();

vm.startPrank(alice);
stEth.submit{value: 10 ether}(address(0));
stEth.approve(address(liquifierInstance), 10 ether);
liquifierInstance.depositWithERC20(address(stEth), 10 ether, address(0));
vm.stopPrank();

assertGe(eETHInstance.balanceOf(alice), 10 ether - 0.1 ether);
assertApproxEqAbs(eETHInstance.balanceOf(alice), 10 ether - 0.5 ether, 0.1 ether);

uint256 aliceQuotedEETH = liquifierInstance.quoteByDiscountedValue(address(stEth), 10 ether);
// alice will actually receive 1 wei less due to the infamous 1 wei rounding corner case
assertApproxEqAbs(eETHInstance.balanceOf(alice), aliceQuotedEETH, 1);
}

function test_deopsit_stEth_and_swap() internal {
function test_deposit_stEth_and_swap() internal {
_setUp(MAINNET_FORK);
uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther();
vm.deal(alice, 100 ether);
Expand All @@ -101,7 +110,7 @@ contract LiquifierTest is TestSetup {
lpTvl = liquidityPoolInstance.getTotalPooledEther();
}

function test_deopsit_stEth_with_explicit_permit() public {
function test_deposit_stEth_with_explicit_permit() public {
initializeRealisticFork(MAINNET_FORK);
setUpLiquifier(MAINNET_FORK);

Expand Down Expand Up @@ -136,56 +145,6 @@ contract LiquifierTest is TestSetup {
liquifierInstance.depositWithERC20WithPermit(address(stEth), 1 ether, address(0), permitInput2);
}

function test_withdrawal_of_non_restaked_stEth() public {
test_deposit_stEth();

uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther();
uint256 lpBalance = address(liquidityPoolInstance).balance;
uint256 liquifierStEthTvl = liquifierInstance.getTotalPooledEther(address(stEth));
uint256 liquifierBalance = address(liquifierInstance).balance;

vm.prank(alice);
uint256[] memory reqIds = liquifierInstance.stEthRequestWithdrawal(10 ether);

assertApproxEqAbs(liquifierInstance.getTotalPooledEther(address(stEth)), liquifierStEthTvl, 1);

bytes32 FINALIZE_ROLE = liquifierInstance.lidoWithdrawalQueue().FINALIZE_ROLE();
address finalize_role = liquifierInstance.lidoWithdrawalQueue().getRoleMember(FINALIZE_ROLE, 0);

// The redemption is approved by the Lido
vm.startPrank(finalize_role);
uint256 currentRate = stEth.getTotalPooledEther() * 1e27 / stEth.getTotalShares();
(uint256 ethToLock, uint256 sharesToBurn) = liquifierInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate);
liquifierInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate);
vm.stopPrank();

// The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract
uint256 lastCheckPointIndex = liquifierInstance.lidoWithdrawalQueue().getLastCheckpointIndex();
uint256[] memory hints = liquifierInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex);

vm.prank(alice);
liquifierInstance.stEthClaimWithdrawals(reqIds, hints);

assertApproxEqAbs(liquifierInstance.getTotalPooledEther(address(stEth)), liquifierStEthTvl - 10 ether, 1 gwei);
assertApproxEqAbs(address(liquifierInstance).balance, liquifierBalance + 10 ether, 1 gwei);

// The ether.fi admin withdraws the ETH from the liquifier contract to the liquidity pool contract
vm.prank(alice);
liquifierInstance.withdrawEther();

assertApproxEqAbs(address(liquidityPoolInstance).balance, lpBalance + 10 ether + liquifierBalance, 1 gwei);
}

function test_stEthRequestWithdrawal() public {
test_deposit_stEth();

vm.startPrank(alice);
liquifierInstance.stEthRequestWithdrawal(1 ether);
liquifierInstance.stEthRequestWithdrawal(5 ether);
liquifierInstance.stEthRequestWithdrawal();
vm.stopPrank();
}

function _enable_deposit(address _strategy) internal {
IEigenLayerStrategyTVLLimits strategyTVLLimits = IEigenLayerStrategyTVLLimits(_strategy);

Expand All @@ -197,62 +156,6 @@ contract LiquifierTest is TestSetup {
vm.stopPrank();
}

function test_pancacke_wbETH_swap() internal {
initializeRealisticFork(MAINNET_FORK);
setUpLiquifier(MAINNET_FORK);

uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther();
uint256 lpBalance = address(liquidityPoolInstance).balance;

uint256 inputAmount = 50 ether;

vm.startPrank(alice);

vm.expectRevert("Too little received");
liquifierInstance.pancakeSwapForEth(address(wbEth), inputAmount, 500, 2 * inputAmount, 3600);

uint256 beforeTVL = liquidityPoolInstance.getTotalPooledEther();
uint256 beforeBalance = address(liquifierInstance).balance;

uint256 exchangeRate = IWBETH(address(wbEth)).exchangeRate();
uint256 maxSlippageBp = 50; // 0.5%
uint256 minOutput = (exchangeRate * inputAmount * (10000 - maxSlippageBp)) / 10000 / 1e18;
liquifierInstance.pancakeSwapForEth(address(wbEth), inputAmount, 500, minOutput, 3600);

assertGe(address(liquifierInstance).balance, beforeBalance + minOutput);
assertEq(liquidityPoolInstance.getTotalPooledEther(), beforeTVL); // does not change till Oracle updates

vm.stopPrank();
}

function test_pancacke_cbETH_swap() internal {
initializeRealisticFork(MAINNET_FORK);
setUpLiquifier(MAINNET_FORK);

uint256 lpTvl = liquidityPoolInstance.getTotalPooledEther();
uint256 lpBalance = address(liquidityPoolInstance).balance;

uint256 inputAmount = 50 ether;

vm.startPrank(alice);

vm.expectRevert("Too little received");
liquifierInstance.pancakeSwapForEth(address(cbEth), inputAmount, 500, 2 * inputAmount, 3600);

uint256 beforeTVL = liquidityPoolInstance.getTotalPooledEther();
uint256 beforeBalance = address(liquifierInstance).balance;

uint256 exchangeRate = IWBETH(address(cbEth)).exchangeRate();
uint256 maxSlippageBp = 50; // 0.5%
uint256 minOutput = (exchangeRate * inputAmount * (10000 - maxSlippageBp)) / 10000 / 1e18;
liquifierInstance.pancakeSwapForEth(address(cbEth), inputAmount, 500, minOutput, 3600);

assertGe(address(liquifierInstance).balance, beforeBalance + minOutput);
assertEq(liquidityPoolInstance.getTotalPooledEther(), beforeTVL); // does not change till Oracle updates

vm.stopPrank();
}

function _setup_L1SyncPool() internal {
initializeRealisticFork(MAINNET_FORK);
setUpLiquifier(MAINNET_FORK);
Expand Down
21 changes: 0 additions & 21 deletions test/TestSetup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1424,27 +1424,6 @@ contract TestSetup is Test {
}
}

function _finalizeLidoWithdrawals(uint256[] memory reqIds) internal {
bytes32 FINALIZE_ROLE = liquifierInstance.lidoWithdrawalQueue().FINALIZE_ROLE();
address finalize_role = liquifierInstance.lidoWithdrawalQueue().getRoleMember(FINALIZE_ROLE, 0);

// The redemption is approved by the Lido
vm.startPrank(finalize_role);
uint256 currentRate = stEth.getTotalPooledEther() * 1e27 / stEth.getTotalShares();
(uint256 ethToLock, uint256 sharesToBurn) = liquifierInstance.lidoWithdrawalQueue().prefinalize(reqIds, currentRate);
liquifierInstance.lidoWithdrawalQueue().finalize(reqIds[reqIds.length-1], currentRate);
vm.stopPrank();

// The ether.fi admin claims the finalized withdrawal, which sends the ETH to the liquifier contract
vm.startPrank(alice);
uint256 lastCheckPointIndex = liquifierInstance.lidoWithdrawalQueue().getLastCheckpointIndex();
uint256[] memory hints = liquifierInstance.lidoWithdrawalQueue().findCheckpointHints(reqIds, 1, lastCheckPointIndex);
liquifierInstance.stEthClaimWithdrawals(reqIds, hints);

liquifierInstance.withdrawEther();
vm.stopPrank();
}

function _prepareForDepositData(uint256[] memory _validatorIds, uint256 _depositAmount) internal returns (IStakingManager.DepositData[] memory) {
IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](_validatorIds.length);
bytes[] memory pubKey = new bytes[](_validatorIds.length);
Expand Down

0 comments on commit 2175e18

Please sign in to comment.