Summary
The owner or manager can not liquidate through StabilityPool.sol#liquidateBorrower().
Vulnerability Details
StabilityPool.sol#liquidateBorrower() function is as follows.
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
@> uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
Here, LendingPool.sol#getUserDebt() function is as follows.
File: LendingPool.sol
function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
@> return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}
As we can see above, it does not consider debt increased by elapsed time.
File: LendingPool.sol
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
@> ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
...
}
Here, ReserveLibrary.sol#updateReserveState() function is as follows.
function updateReserveState(ReserveData storage reserve,ReserveRateData storage rateData) internal {
@> updateReserveInterests(reserve, rateData);
}
...
function updateReserveInterests(ReserveData storage reserve,ReserveRateData storage rateData) internal {
@> uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
uint256 oldLiquidityIndex = reserve.liquidityIndex;
if (oldLiquidityIndex < 1) revert LiquidityIndexIsZero();
reserve.liquidityIndex = calculateLiquidityIndex(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
@> reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
@> timeDelta,
reserve.usageIndex
);
reserve.lastUpdateTimestamp = uint40(block.timestamp);
emit ReserveInterestsUpdated(reserve.liquidityIndex, reserve.usageIndex);
}
From upper implementation, if time is elapsed from reserve.lastUpdateTimestamp the owner or manager cannot liquidate through StabilityPool.sol#liquidateBorrower() function.
Impact
If time is elapsed from reserve.lastUpdateTimestamp the owner or manager cannot liquidate through StabilityPool.sol#liquidateBorrower() function.
Tools Used
Manual review
Recommendations
Modify LendingPool.sol#getUserDebt() function so that it returns current usageIndex by considering elapsed time.