Summary
The debt amount of the user will return the actual amount a user is owning but because of the lack of a previous update call, this amount will be smaller than the actual amount causing the liquidation debt causing the transaction to revert due to insufficient allowance.
Vulnerability Details
Update call is made after userdebt has been approved
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
@audit>> uint256 userDebt = lendingPool.getUserDebt(userAddress);
@audit>> 1. uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
@audit>> 2. if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
@audit>> 3. bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
@audit>> 4. lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
The index returned will be state has there is no update call before this and hence the debt amount approved will be lesser than the actual amount needed.
@audit 4. should be done first else the entire checks and approval will not work accordingly.
Debt amount calculated will not be accurate
Alice debt is liquidatable
present index is 1.208163179e27 ray
with a debt of 1000e18 debt token.
Stale index is 1.208012100e27
Because there is no update call we calculate the debt amount based on the stale amount and approve Crvusd to cover that portion
Call is made to lending pool to finalize liquidation
The recent debt is calculated and amount to be send in is based on this amount causing the entire call to revert
Note check @audit 3. will also not be adequate has index is stale
* @notice Allows the Stability Pool to finalize the liquidation after the grace period has expired
* @param userAddress The address of the user being liquidated
*/
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
@audit>> 1. ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
@audit>>2. uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
@audit>> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}
Impact
Liquidation call will always revert because insufficient approval is given to the lending pool
Tools Used
Manual Review
Recommendations
Update the state before calculating the amount to approve for the liquidation process