Platform: Code4rena
Start Date: 12/08/2022
Pot Size: $50,000 USDC
Total HM: 15
Participants: 120
Period: 5 days
Judge: Justin Goro
Total Solo HM: 6
Id: 153
League: ETH
Rank: 7/120
Findings: 3
Award: $2,520.45
π Selected for report: 0
π Solo Findings: 0
2282.0928 USDC - $2,282.09
In the liquidateClean function of the FraxlendPairCore contract, when the debt is fully liquidated, the liquidator receives more collateral than when it is partially liquidated, due to the fact that cleanLiquidationFee is larger than dirtyLiquidationFee. In the liquidate function used for partial liquidation, cleanLiquidationFee is used to calculate the collateral obtained by the liquidator, which means that the contract discourages the user from calling the liquidateClean function for full liquidation, since the user receives the same percentage of liquidation fees for partial and full liquidation. Since the liquidator has no incentive to perform a full liquidation, this may increase the bad debt in the contract
https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L929-L933 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L193-L194
None
Use dirtyLiquidationFee in the liquidate function to calculate the liquidation fee
function liquidate(uint256 _shares, address _borrower) external whenNotPaused nonReentrant approvedLender(msg.sender) returns (uint256 _collateralForLiquidator) { _addInterest(); // Get best available exchange rate uint256 _exchangeRate = _updateExchangeRate(); if (_isSolvent(_borrower, _exchangeRate)) { revert BorrowerSolvent(); } VaultAccount memory _totalBorrow = totalBorrow; // Determine how much of the borrow and collateral will be repaid _collateralForLiquidator = (((_totalBorrow.toAmount(_shares, false) * _exchangeRate) / EXCHANGE_PRECISION) * - (LIQ_PRECISION + cleanLiquidationFee)) / + (LIQ_PRECISION + dirtyLiquidationFee)) / LIQ_PRECISION;
#0 - DrakeEvans
2022-09-06T12:33:16Z
Disagree with severity, both liquidate and liquidateClean can be used interchangeably. Final launch will only include one. Even without liquidateClean, contract is still secure.
#1 - 0xA5DF
2022-09-06T18:06:35Z
Even without liquidateClean, contract is still secure.
My submission for this bug (#141) focuses more on this scenario - if liquidate()
will be the function that will be used, bad debt wouldn't be marked off. Leading to a scenario where the last lender(s) to withdraw will bear all the losses of bad debt(s).
#2 - gititGoro
2022-10-03T23:02:19Z
Duplicate of #141
π Selected for report: auditor0517
Also found by: 0xA5DF, _Adam, cccz, minhquanym, minhtrng, zzzitron
192.5076 USDC - $192.51
According to the comments, dirtyLiquidationFee should be 90% of cleanLiquidationFee, since LIQ_PRECISION = 1e5, dirtyLiquidationFee here is actually 9% of cleanLiquidationFee.
cleanLiquidationFee = _liquidationFee; dirtyLiquidationFee = (_liquidationFee * 9000) / LIQ_PRECISION; // 90% of clean fee
In the liquidateClean function, the dirtyLiquidationFee is used to decide the amount of collateral that the user should get when they are not fully liquidated, and a decrease in the dirtyLiquidationFee will reduce the amount of collateral that the liquidator gets, thus making the liquidator suffer a loss.
uint256 _optimisticCollateralForLiquidator = (_liquidationAmountInCollateralUnits * (LIQ_PRECISION + cleanLiquidationFee)) / LIQ_PRECISION; // Because interest accrues every block, _liquidationAmountInCollateralUnits from a few lines up is an ever increasing value // This means that leftoverCollateral can occasionally go negative by a few hundred wei (cleanLiqFee premium covers this for liquidator) _leftoverCollateral = (_userCollateralBalance.toInt256() - _optimisticCollateralForLiquidator.toInt256()); // If cleanLiquidation fee results in no leftover collateral, give liquidator all the collateral // This will only be true when there liquidator is cleaning out the position _collateralForLiquidator = _leftoverCollateral <= 0 ? _userCollateralBalance : (_liquidationAmountInCollateralUnits * (LIQ_PRECISION + dirtyLiquidationFee)) / LIQ_PRECISION;
https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L194-L195 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L979-L990
None
Change 9000 to 90000
- dirtyLiquidationFee = (_liquidationFee * 9000) / LIQ_PRECISION; // 90% of clean fee + dirtyLiquidationFee = (_liquidationFee * 90000) / LIQ_PRECISION; // 90% of clean fee
#0 - 0xA5DF
2022-08-17T20:42:49Z
Duplicate of #132
#1 - amirnader-ghazvini
2022-08-29T18:46:33Z
Duplicate of #238
π Selected for report: 0x1f8b
Also found by: 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xmatt, 0xsolstars, Aymen0909, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, EthLedger, Funen, IllIllI, JC, Junnon, Lambda, LeoS, MiloTruck, Noah3o6, PaludoX0, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, SaharAP, Sm4rty, SooYa, The_GUILD, TomJ, Waze, Yiko, _Adam, __141345__, a12jmx, ak1, asutorufos, auditor0517, ayeslick, ballx, beelzebufo, berndartmueller, bin2chen, brgltd, c3phas, cRat1st0s, cccz, cryptonue, cryptphi, d3e4, delfin454000, dipp, djxploit, durianSausage, dy, erictee, fatherOfBlocks, gogo, gzeon, hyh, ignacio, kyteg, ladboy233, medikko, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, ret2basic, robee, sach1r0, simon135, sryysryy, tabish, yac, yash90, zzzitron
45.8482 USDC - $45.85
Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. They must first be approved by zero and then the actual allowance must be approved.
https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L1103-L1104 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L1184-L1185
None
Set the allowance to zero immediately before each of the existing approve() calls.
On FraxlendPairCore.sol, we are using latestRoundData, but there is no check if the return value indicates stale data.
if (oracleMultiply != address(0)) { (, int256 _answer, , , ) = AggregatorV3Interface(oracleMultiply).latestRoundData(); if (_answer <= 0) { revert OracleLTEZero(oracleMultiply); } _price = _price * uint256(_answer); } if (oracleDivide != address(0)) { (, int256 _answer, , , ) = AggregatorV3Interface(oracleDivide).latestRoundData(); if (_answer <= 0) { revert OracleLTEZero(oracleDivide); } _price = _price / uint256(_answer); }
This could lead to stale prices according to the Chainlink documentation:
https://docs.chain.link/docs/historical-price-data/#historical-rounds https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round
None
if (oracleMultiply != address(0)) { (uint80 roundID, int256 _answer, , uint256 timestamp, uint80 answeredInRound) = AggregatorV3Interface(oracleMultiply).latestRoundData(); require(answeredInRound >= roundID, "Stale price"); require(timestamp != 0,"Round not complete"); if (_answer <= 0) { revert OracleLTEZero(oracleMultiply); } _price = _price * uint256(_answer); } if (oracleDivide != address(0)) { (uint80 roundID, int256 _answer, , uint256 timestamp, uint80 answeredInRound) = AggregatorV3Interface(oracleDivide).latestRoundData(); require(answeredInRound >= roundID, "Stale price"); require(timestamp != 0,"Round not complete"); if (_answer <= 0) { revert OracleLTEZero(oracleDivide); } _price = _price / uint256(_answer); }
#0 - gititGoro
2022-10-06T16:54:11Z
Nicely documented but unfortunately both issues are invalid since oracle config and token idiosyncrasies are explicitly listed in the documentation as out of scope.