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

Integrate Liquifier Changes #201

Merged
merged 5 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
138 changes: 36 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;
seongyun-ko marked this conversation as resolved.
Show resolved Hide resolved

RoleRegistry public roleRegistry;

BucketRateLimiter public rateLimiter;
Expand Down Expand Up @@ -157,15 +159,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 +198,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 +235,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 +267,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 +285,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 +302,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
Loading