Skip to content

Commit

Permalink
Merge branch 'master' into syko/operations/20241015-2
Browse files Browse the repository at this point in the history
Signed-off-by: syko <[email protected]>
  • Loading branch information
seongyun-ko authored Oct 22, 2024
2 parents e07755c + 1b0cb93 commit eb81b99
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 29 deletions.
13 changes: 13 additions & 0 deletions operations/20241018_upgrade_liquifier.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{ "version": "1.0", "chainId": "1", "meta": { "name": "Transactions Batch", "description": "", "txBuilderVersion": "1.16.5", "createdFromSafeAddress": "0xcdd57D11476c22d265722F68390b036f3DA48c21" }, "transactions": [
{
"to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761",
"value": "0",
"data": "0x01d5062a0000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000005769ff35545b0bbfa27cc97c9407c5ed9d39545500000000000000000000000000000000000000000000000000000000"
}
,
{
"to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761",
"value": "0",
"data": "0x134008d30000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000005769ff35545b0bbfa27cc97c9407c5ed9d39545500000000000000000000000000000000000000000000000000000000"
}
] }
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: 'Liquifier: pricing of stETH via CurvePool & fixed rate fee'

---

# Liquifier: pricing of stETH via CurvePool & fixed rate fee

**PR**: https://github.com/etherfi-protocol/smart-contracts/pull/188

## Summary

This PR adds the option to price `stETH` via a `ETH/stETH` Curve pool. Additionally, it adds the option of applying a fixed rate fee in the `depositWithERC20(...)` function.

---

## Findings

### [Medium] Spot prices from Curve can be manipulated

**File(s)**: [`Liquifier.sol`](https://github.com/etherfi-protocol/smart-contracts/blob/1f95dcd0677f7ffa387e70c2240981c478a701b2/src/Liquifier.sol#L404)

**Description**: The use of `CurvePool` as quoter has the goal of `removing the ability to swap stEth/eETH 1:1 without slippage`. To get the price from the `CurvePool` the `get_dy(...)` function is used.

```solidity
...
if (_token == address(lido)) {
if (quoteStEthWithCurve) {
return _min(_amount, ICurvePoolQuoter1(address(stEth_Eth_Pool)).get_dy(1, 0, _amount));
} else {
return _amount; /// 1:1 from stETH to eETH
}
...
```

The `get_dy(...)` function returns the result of swapping `amount` of tokens at the current state of the pool. The result of this function can be easily manipulated by swapping in the `CurvePool`. The returned value could be manipulated to still enforce the use of a `1:1` rate.

**Recommendation(s)**: Consider using a different method to quote the `stEth` that is not easily manipulable. The use of other oracle solutions like `TWAPs` or `Chainlink Oracles` is recommended.

**Status**: Unresolved

**Update from the client**:
49 changes: 26 additions & 23 deletions src/Liquifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
uint32 public DEPRECATED_eigenLayerWithdrawalClaimGasCost;
uint32 public timeBoundCapRefreshInterval; // seconds

bool public DEPRECATED_quoteStEthWithCurve;
bool public quoteStEthWithCurve;

uint128 public DEPRECATED_accumulatedFee;

Expand Down Expand Up @@ -101,6 +101,7 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
error NotRegistered();
error WrongOutput();
error IncorrectCaller();
error IncorrectAmount();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -132,20 +133,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
DEPRECATED_eigenLayerWithdrawalClaimGasCost = 150_000;
}

function initializeOnUpgrade(address _eigenLayerDelegationManager, address _pancakeRouter) external onlyOwner {
// Disable the deposits on {cbETH, wBETH}
updateDepositCap(address(cbEth), 0, 0);
updateDepositCap(address(wbEth), 0, 0);

pancakeRouter = IPancackeV3SwapRouter(_pancakeRouter);
eigenLayerDelegationManager = IDelegationManager(_eigenLayerDelegationManager);
}

function initializeL1SyncPool(address _l1SyncPool) external onlyOwner {
if (l1SyncPool != address(0)) revert();
l1SyncPool = _l1SyncPool;
}

receive() external payable {}

/// the users mint eETH given the queued withdrawal for their LRT with withdrawer == address(this)
Expand Down Expand Up @@ -181,11 +168,8 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab

// 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);
require(!isDepositCapReached(_token, dx), "CAPPED");

uint256 eEthShare = liquidityPool.depositToRecipient(msg.sender, dx, _referral);
Expand Down Expand Up @@ -227,7 +211,8 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
}

function stEthRequestWithdrawal(uint256 _amount) public onlyAdmin returns (uint256[] memory) {
if (_amount < lidoWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT() || _amount < lido.balanceOf(address(this))) revert NotEnoughBalance();
if (_amount < lidoWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()) revert IncorrectAmount();
if (_amount > lido.balanceOf(address(this))) revert NotEnoughBalance();

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

Expand Down Expand Up @@ -305,6 +290,14 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab
pausers[_address] = _isPauser;
}

function updateDiscountInBasisPoints(address _token, uint16 _discountInBasisPoints) external onlyAdmin {
tokenInfos[_token].discountInBasisPoints = _discountInBasisPoints;
}

function updateQuoteStEthWithCurve(bool _quoteStEthWithCurve) external onlyAdmin {
quoteStEthWithCurve = _quoteStEthWithCurve;
}

//Pauses the contract
function pauseContract() external onlyPauser {
_pause();
Expand Down Expand Up @@ -407,7 +400,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 @@ -420,6 +417,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 verifyQueuedWithdrawal(address _user, IDelegationManager.Withdrawal calldata _queuedWithdrawal) public view returns (bytes32) {
require(_queuedWithdrawal.staker == _user && _queuedWithdrawal.withdrawer == address(this), "wrong depositor/withdrawer");
for (uint256 i = 0; i < _queuedWithdrawal.strategies.length; i++) {
Expand Down Expand Up @@ -523,7 +527,6 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab

function _completeWithdrawals(IDelegationManager.Withdrawal memory _queuedWithdrawal) internal {
bytes32 withdrawalRoot = eigenLayerDelegationManager.calculateWithdrawalRoot(_queuedWithdrawal);
if (!isRegisteredQueuedWithdrawals[withdrawalRoot]) revert NotRegistered();

uint256 numStrategies = _queuedWithdrawal.strategies.length;
for (uint256 i = 0; i < numStrategies; i++) {
Expand Down
17 changes: 12 additions & 5 deletions test/EtherFiTimelock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -318,20 +318,27 @@ contract TimelockTest is TestSetup {
initializeRealisticFork(MAINNET_FORK);
address target = address(managerInstance);
bytes4 selector = 0x3ccc861d;

bytes memory data = abi.encodeWithSelector(EtherFiNodesManager.updateAllowedForwardedExternalCalls.selector, selector, 0x7750d328b314EfFa365A0402CcfD489B80B0adda, true);
_execute_timelock(target, data, true, true, true, true);
}

function test_update_treasury() public {
initializeRealisticFork(MAINNET_FORK);
{
initializeRealisticFork(MAINNET_FORK);
address target =
address(liquidityPoolInstance);
address target = address(liquidityPoolInstance);
bytes memory data = abi.encodeWithSelector(LiquidityPool.setTreasury.selector, 0x0c83EAe1FE72c390A02E426572854931EefF93BA);
_execute_timelock(target, data, true, true, true, true);
}
}

function test_upgrade_liquifier() public {
initializeRealisticFork(MAINNET_FORK);
{
address target = address(liquifierInstance);
bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x5769ff35545B0BBFA27cc97C9407C5ed9d395455);
_execute_timelock(target, data, true, true, true, true);
}
}
}

// {"version":"1.0","chainId":"1
11 changes: 10 additions & 1 deletion test/Liquifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,22 @@ 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 {
Expand Down

0 comments on commit eb81b99

Please sign in to comment.