Core Contracts

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

Systemic Overfunding and Fund Misallocation in Liquidations

Summary

A double scaling error in liquidateBorrower() results in systematic overfunding of liquidations, causing persistent fund misallocation in the Stability Pool. Because getUserDebt() already applies rayMul(reserve.usageIndex), calling rayMul(getNormalizedDebt()) again in liquidateBorrower() inflates the perceived user debt, leading to unnecessary excess fund transfers. This issue is exacerbated over time as the usageIndex increases exponentially, forcing managers to continuously top up Stability Pool reserves wastefully with more allocated funds than intended. The flaw creates an inefficient liquidation mechanism, drains protocol resources, and distorts Stability Pool reserves.

Note: liquidateBorrower() will also need to call lendingPool.updateState() before getting the user's debt from the LendingPool as I have separately reported.

Vulnerability Details

In liquidateBorrower(), the contract first retrieves the user’s total debt:

StabilityPool.sol#L451-L452

// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);

However, getUserDebt() already applies rayMul(reserve.usageIndex):

LendingPool.sol#L579-L582

/**
* @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);
}

Instead of using this correctly scaled debt amount, liquidateBorrower() scales it again:

StabilityPool.sol#L453

uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());

This doubles the scaling effect, overestimating the user’s debt and causing unnecessary excess prefunding of Stability Pool with crvUSDToken in order for the following check to pass:

StabilityPool.sol#L457-L458

uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();

The thing is managers query lendingPool.getUserDebt() and assume it returns the correct amount needed for liquidation. However, due to the double scaling issue, the contract believes there is insufficient crvUSDBalance and reverts liquidations unless extra funds are allocated. If managers don’t notice this flaw, they may continuously send additional funds to "fix" the issue, leading to fund leakage over multiple liquidation cycles.

Since Stability Pool funds are pre-allocated, managers may opt to overfund liquidations rather than debugging why their transactions fail. This turns the Stability Pool into a constantly misused liquidity sink, making it impossible to accurately track available reserves. And, there is no rescueToken() or refund logic for the excess crvUSDToken sent in.

Over time, usageIndex increases exponentially, so the discrepancy widens, amplifying the cascading wastes.

Impact

  • Perpetual Stability Pool Drainage:
    Overfunded liquidations reduce Stability Pool efficiency, depleting protocol reserves over time.

  • Systemic Fund Misallocation:
    The Stability Pool no longer holds an accurate reserve of available funds, causing unexpected capital inefficiencies.

  • Increased Liquidation Costs for Users:
    Managers may unnecessarily lose more assets in liquidations due to artificially inflated debt amounts.

  • Protocol-Wide Capital Inefficiency:
    The protocol mismanages funds, requiring either constant manager intervention or excess liquidity to compensate for miscalculations.

Tools Used

Manual

Recommendations

Consider making the following refactoring:

StabilityPool.sol#L453

- uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
+ uint256 scaledUserDebt = userDebt; // No extra scaling needed
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.