From f052425eafa5778eda17ce83cde98e1d1a927cc4 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 16 Oct 2024 10:43:12 +0900 Subject: [PATCH 1/8] use CurvePool price to determine the minimum mint amount for stETH deposit --- src/Liquifier.sol | 26 ++++++++++---------------- test/Liquifier.t.sol | 3 +++ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Liquifier.sol b/src/Liquifier.sol index 89df6a1b..7d1d5ec3 100644 --- a/src/Liquifier.sol +++ b/src/Liquifier.sol @@ -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; @@ -132,20 +132,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) @@ -305,6 +291,10 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab pausers[_address] = _isPauser; } + function updateQuoteStEthWithCurve(bool _quoteStEthWithCurve) external onlyOwner { + quoteStEthWithCurve = _quoteStEthWithCurve; + } + //Pauses the contract function pauseContract() external onlyPauser { _pause(); @@ -407,7 +397,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)) { diff --git a/test/Liquifier.t.sol b/test/Liquifier.t.sol index 7daf9bad..be5a4b49 100644 --- a/test/Liquifier.t.sol +++ b/test/Liquifier.t.sol @@ -85,6 +85,9 @@ contract LiquifierTest is TestSetup { vm.deal(alice, 100 ether); + vm.prank(liquifierInstance.owner()); + liquifierInstance.updateQuoteStEthWithCurve(true); + vm.startPrank(alice); stEth.submit{value: 10 ether}(address(0)); stEth.approve(address(liquifierInstance), 10 ether); From 6bb7c5a5f9d1d996487e988ce7b2a46d9c481e4d Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 16 Oct 2024 10:49:13 +0900 Subject: [PATCH 2/8] add two features: pricing stETH using CurvePool, fixed fee to stETH deposit --- src/Liquifier.sol | 6 +++++- test/Liquifier.t.sol | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Liquifier.sol b/src/Liquifier.sol index 7d1d5ec3..01f71abc 100644 --- a/src/Liquifier.sol +++ b/src/Liquifier.sol @@ -291,7 +291,11 @@ contract Liquifier is Initializable, UUPSUpgradeable, OwnableUpgradeable, Pausab pausers[_address] = _isPauser; } - function updateQuoteStEthWithCurve(bool _quoteStEthWithCurve) external onlyOwner { + function updateDiscountInBasisPoints(address _token, uint16 _discountInBasisPoints) external onlyAdmin { + tokenInfos[_token].discountInBasisPoints = _discountInBasisPoints; + } + + function updateQuoteStEthWithCurve(bool _quoteStEthWithCurve) external onlyAdmin { quoteStEthWithCurve = _quoteStEthWithCurve; } diff --git a/test/Liquifier.t.sol b/test/Liquifier.t.sol index be5a4b49..7f6936d7 100644 --- a/test/Liquifier.t.sol +++ b/test/Liquifier.t.sol @@ -85,8 +85,10 @@ contract LiquifierTest is TestSetup { vm.deal(alice, 100 ether); - vm.prank(liquifierInstance.owner()); + 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)); @@ -94,7 +96,7 @@ contract LiquifierTest is TestSetup { 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); } function test_deopsit_stEth_and_swap() internal { From d6aede38a1d1438733c827a59a44e07c1f160245 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Wed, 16 Oct 2024 11:46:25 +0900 Subject: [PATCH 3/8] fix the error in 'stEthRequestWithdrawal' --- src/Liquifier.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Liquifier.sol b/src/Liquifier.sol index 01f71abc..90bdb0ee 100644 --- a/src/Liquifier.sol +++ b/src/Liquifier.sol @@ -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() { @@ -213,7 +214,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); From d60ed5a947ec259fb1a9b125561c0262dc62b08c Mon Sep 17 00:00:00 2001 From: jtfirek Date: Wed, 16 Oct 2024 10:38:23 -0500 Subject: [PATCH 4/8] discount pricing function --- src/Liquifier.sol | 14 +++++++++----- test/Liquifier.t.sol | 4 ++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Liquifier.sol b/src/Liquifier.sol index 90bdb0ee..ddc84dbf 100644 --- a/src/Liquifier.sol +++ b/src/Liquifier.sol @@ -168,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); @@ -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++) { diff --git a/test/Liquifier.t.sol b/test/Liquifier.t.sol index 7f6936d7..31ad5782 100644 --- a/test/Liquifier.t.sol +++ b/test/Liquifier.t.sol @@ -97,6 +97,10 @@ contract LiquifierTest is TestSetup { vm.stopPrank(); 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 { From be643a49e8abd1abbc86959a5697abcd45df62e6 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 17 Oct 2024 12:36:25 +0900 Subject: [PATCH 5/8] remove 'isRegisteredQueuedWithdrawals' check to process the pending-migrated withdrawals --- src/Liquifier.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Liquifier.sol b/src/Liquifier.sol index 90bdb0ee..ab3c49a7 100644 --- a/src/Liquifier.sol +++ b/src/Liquifier.sol @@ -523,7 +523,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++) { From 962afe3d7ad79eadedb7697e6de92db9847e4163 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Thu, 17 Oct 2024 13:18:06 +0900 Subject: [PATCH 6/8] add timelock txns --- operations/20241018_upgrade_liquifier.json | 13 +++++++++++++ test/EtherFiTimelock.sol | 10 +++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 operations/20241018_upgrade_liquifier.json diff --git a/operations/20241018_upgrade_liquifier.json b/operations/20241018_upgrade_liquifier.json new file mode 100644 index 00000000..1eff3268 --- /dev/null +++ b/operations/20241018_upgrade_liquifier.json @@ -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": "0x01d5062a0000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000006b6d4e2dfcb864c83e29641429c528e8016bacdf00000000000000000000000000000000000000000000000000000000" +} +, +{ + "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", + "value": "0", + "data": "0x134008d30000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000006b6d4e2dfcb864c83e29641429c528e8016bacdf00000000000000000000000000000000000000000000000000000000" +} +] } diff --git a/test/EtherFiTimelock.sol b/test/EtherFiTimelock.sol index 62c7935c..8cb56dc2 100644 --- a/test/EtherFiTimelock.sol +++ b/test/EtherFiTimelock.sol @@ -318,10 +318,18 @@ 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_upgrade_liquifier() public { + initializeRealisticFork(MAINNET_FORK); + { + address target = address(liquifierInstance); + bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x6B6D4e2DFcB864c83E29641429C528e8016BaCDf); + _execute_timelock(target, data, true, true, true, true); + } + } } // {"version":"1.0","chainId":"1 \ No newline at end of file From ee3ad10c076a77d3673c3cbafca5a6ceacf7c699 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 18 Oct 2024 06:10:21 +0900 Subject: [PATCH 7/8] add the audit report --- ...of stETH via CurvePool & fixed rate fee.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 proposals/references/Liquifier_ pricing of stETH via CurvePool & fixed rate fee.md diff --git a/proposals/references/Liquifier_ pricing of stETH via CurvePool & fixed rate fee.md b/proposals/references/Liquifier_ pricing of stETH via CurvePool & fixed rate fee.md new file mode 100644 index 00000000..5aed39ec --- /dev/null +++ b/proposals/references/Liquifier_ pricing of stETH via CurvePool & fixed rate fee.md @@ -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**: \ No newline at end of file From bd08c37b282c18ab36d6c4520b1b5e21b6732a10 Mon Sep 17 00:00:00 2001 From: ReposCollector Date: Fri, 18 Oct 2024 06:47:14 +0900 Subject: [PATCH 8/8] re-deploy --- operations/20241018_upgrade_liquifier.json | 4 ++-- test/EtherFiTimelock.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/operations/20241018_upgrade_liquifier.json b/operations/20241018_upgrade_liquifier.json index 1eff3268..fe0dedf6 100644 --- a/operations/20241018_upgrade_liquifier.json +++ b/operations/20241018_upgrade_liquifier.json @@ -2,12 +2,12 @@ { "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", "value": "0", - "data": "0x01d5062a0000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000006b6d4e2dfcb864c83e29641429c528e8016bacdf00000000000000000000000000000000000000000000000000000000" + "data": "0x01d5062a0000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000005769ff35545b0bbfa27cc97c9407c5ed9d39545500000000000000000000000000000000000000000000000000000000" } , { "to": "0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761", "value": "0", - "data": "0x134008d30000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000006b6d4e2dfcb864c83e29641429c528e8016bacdf00000000000000000000000000000000000000000000000000000000" + "data": "0x134008d30000000000000000000000009ffdf407cde9a93c47611799da23924af3ef764f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000243659cfe60000000000000000000000005769ff35545b0bbfa27cc97c9407c5ed9d39545500000000000000000000000000000000000000000000000000000000" } ] } diff --git a/test/EtherFiTimelock.sol b/test/EtherFiTimelock.sol index 8cb56dc2..71227e2e 100644 --- a/test/EtherFiTimelock.sol +++ b/test/EtherFiTimelock.sol @@ -326,7 +326,7 @@ contract TimelockTest is TestSetup { initializeRealisticFork(MAINNET_FORK); { address target = address(liquifierInstance); - bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x6B6D4e2DFcB864c83E29641429C528e8016BaCDf); + bytes memory data = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, 0x5769ff35545B0BBFA27cc97C9407C5ed9d395455); _execute_timelock(target, data, true, true, true, true); } }