Summary
Liquidations may be DOSed because user's debt is incorrectly multiplied by usage index (normalizedDebt). User debt is falsely inflated and liquidateBorrower
reverts because it consider it has insufficient asset to cover the debt.
Vulnerability Details
DebtToken is a rebasing token, meaning it increases in balance not in value. DebtToken balance is derived from the scaledBalance
and scaledBalance
remains constant for an user unless they borrow more or repay.
The actual debt a user has to repay is returned by LendingPool::getUserDebt. user.scaledDebtBalance
represents the amount borrowed scalled (divided) by usageIndex
* @notice Gets the user's debt including interest
* @param userAddress The address of the user
* @return The user's total debt
*/
function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
@> return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
...
@> uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) =
IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
@> user.scaledDebtBalance += scaledAmount;
...
When StabilityPool::liquidateBorrower is called, userDebt, which already includes the interest, is multiplied by usageIndex
: scaledUserDebt = userDebt * usageIndex
.
Then scaledUserDebt
is compared with pool's asset balance and reverts if it's smaller, considering it doesn't have enough funds to cover the debt.
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
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();
Some borrowers may not be liquidated when, in fact, StabilityPool
has enough assets to cover for debt. The chances for this situation to happen is higher for big borrowers that deposited many RAACNfts as collateral, increasing the potential loss as well.
Moreover, since usageIndex
growswith accumulating interest, the ratio at which a user's debt is falsely inflated will increase, making this scenario more feasible.
Impact
Tools Used
Recommendations
Remove following line from liquidateBorrower
;
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
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();
+ if (crvUSDBalance < userDebt) revert InsufficientBalance();
...
}