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: 84/120
Findings: 1
Award: $45.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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.8343 USDC - $45.83
9000
is hardcoded for dirtyLiquidationFee
. If LIQ_PRECISION be changed the calculation becomes incorrect by magnitudes:
cleanLiquidationFee = _liquidationFee; dirtyLiquidationFee = (_liquidationFee * 9000) / LIQ_PRECISION; // 90% of clean fee
Consider introducing DIRTY_LIQ_FEE
to project constants and using it:
cleanLiquidationFee = _liquidationFee; dirtyLiquidationFee = (_liquidationFee * DIRTY_LIQ_FEE) / LIQ_PRECISION; // 90% of clean fee
FraxlendPairDeployer and FraxlendPairCore constructors do not check core configuration variables:
constructor( address _circuitBreaker, address _comptroller, address _timelock, address _fraxlendWhitelist ) Ownable() { CIRCUIT_BREAKER_ADDRESS = _circuitBreaker; COMPTROLLER_ADDRESS = _comptroller; TIME_LOCK_ADDRESS = _timelock; FRAXLEND_WHITELIST_ADDRESS = _fraxlendWhitelist; }
CIRCUIT_BREAKER_ADDRESS = _circuitBreaker; COMPTROLLER_ADDRESS = _comptrollerAddress; TIME_LOCK_ADDRESS = _timeLockAddress; FRAXLEND_WHITELIST_ADDRESS = _fraxlendWhitelistAddress;
When these variables set incorrectly the lending pair functionality become mostly unusable
Consider adding validity checks (non-zero addresses and variables range)
It is not checked that at least one oracle is functional:
if (_oracleMultiply != address(0) && !_fraxlendWhitelist.oracleContractWhitelist(_oracleMultiply)) { revert NotOnWhitelist(_oracleMultiply); } if (_oracleDivide != address(0) && !_fraxlendWhitelist.oracleContractWhitelist(_oracleDivide)) { revert NotOnWhitelist(_oracleDivide); } // Write oracleData to storage oracleMultiply = _oracleMultiply; oracleDivide = _oracleDivide; oracleNormalization = _oracleNormalization;
Setting both oracles to zero will imply that everything is always solvent, _isSolvent == true
, as LTV = 0:
function _isSolvent(address _borrower, uint256 _exchangeRate) internal view returns (bool) { if (maxLTV == 0) return true; uint256 _borrowerAmount = totalBorrow.toAmount(userBorrowShares[_borrower], true); if (_borrowerAmount == 0) return true; uint256 _collateralAmount = userCollateralBalance[_borrower]; if (_collateralAmount == 0) return false; uint256 _ltv = (((_borrowerAmount * _exchangeRate) / EXCHANGE_PRECISION) * LTV_PRECISION) / _collateralAmount; return _ltv <= maxLTV; }
Consider requiring in constructor that at least one oracle is a valid address.
Utilizing stale prices in lending protocols yields liquidations of healthy positions and vice versa. Price feed validation is important part of any collateralization management logic.
_updateExchangeRate skips the timestamp/round data returned by AggregatorV3Interface.latestRoundData
and only uses the price:
uint256 _price = uint256(1e36); 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); } _exchangeRate = _price / oracleNormalization;
Consider adding the checks for positive timestamp and introducing the stale price threshold:
https://docs.chain.link/docs/feed-registry/#latestrounddata
(, int256 price, , int256 updatedAt,) = AggregatorV3Interface(oracle).latestRoundData(); require(updatedAt > 0 && updatedAt > PRICE_UPDATE_THRESHOLD, "Stale price");
_redeem() do not checks the allowance, attempting to remove it instead:
if (msg.sender != _owner) { uint256 allowed = allowance(_owner, msg.sender); // NOTE: This will revert on underflow ensuring that allowance > shares if (allowed != type(uint256).max) _approve(_owner, msg.sender, allowed - _shares); }
Consider introducing proper error and perform a require to check that balance is enough
Consider removal of the line:
// NOTE: reverts if _collateralForLiquidator > userCollateralBalance