Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

User debt is incorrectly multiplied by normalizedDebt; Liquidations may be DOSed because `StabilityPool` consider erroneously it has insufficient balance

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

//LendingPool.sol
/**
* @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) {
...
// Update user's scaled debt balance
@> uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
// Mint DebtTokens to the user (scaled amount)
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) =
IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
// Transfer borrowed amount to user
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();
// 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();

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();
...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::liquidateBorrower double-scales debt by multiplying already-scaled userDebt with usage index again, causing liquidations to fail

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.