Core Contracts

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

Double Debt Scaling in StabilityPool Causes Permanent Lock of Liquidation Funds

Summary

The StabilityPool contract incorrectly scales user debt twice during liquidation checks, This causes the StabilityPool to require more crvUSD than actually needed for liquidation, with the excess amount becoming permanently locked in the contract as there is no withdrawal mechanism for crvUSD.

Vulnerability Details

To understand this vulnerability, let's first look at how debt calculation and liquidation work in the protocol:

  1. Debt Calculation in LendingPool:

The LendingPool’s function getUserDebt computes the user’s debt by taking the scaled debt balance and multiplying it by the usageIndex. This usage index represents accrued interest (much like Aave’s liquidity index) so that the returned debt value already reflects interest accrued over time

// LendingPool.sol
function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}
  1. Incorrect Double Scaling in StabilityPool:

In the StabilityPool contract, after retrieving the debt using getUserDebt, the contract performs an extra scaling multiplication by calling rayMul with lendingPool.getNormalizedDebt() again .

// StabilityPool.sol
uint256 userDebt = lendingPool.getUserDebt(userAddress); // First scaling
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()); // Second scaling

This second scaling factor, getNormalizedDebt(), appears to adjust the debt once more. If the value returned by getNormalizedDebt() is greater than 1 (in ray units), this will inflate the computed debt dramatically.

  • The StabilityPool then compares its crvUSD balance against this scaledUserDebt (which is inflated). If the balance is lower than the inflated debt, it prevents liquidation:

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

This leads to a situation where more funds need to be deposited into the StabilityPool to cover what is, in effect, an exaggerated debt value

However, when the actual liquidation occurs in the LendingPool, it uses the correct amount :

// LendingPool.sol
function finalizeLiquidation(address userAddress) external {
// ...
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex); // Single scaling
// ...
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
}

Example Scenario :

  • Initial user debt: 100 crvUSD

  • Usage index: 1.5

  • First scaling in LendingPool: 100 * 1.5 = 150 crvUSD (this is user debt value including accrued interest)

  • Second scaling in StabilityPool: 150 * 1.5 = 225 crvUSD (incorrect , inflated by 75 crvUSD)

The StabilityPool requires 225 crvUSD to be deposited for liquidation, but the LendingPool only uses 150 crvUSD to clear the debt.

The remaining 75 crvUSD becomes permanently locked in the StabilityPool as there is no mechanism to withdraw these excess funds.

Impact

  • Liquidators must over-deposit funds (up to x% more than needed depends on how big usageIndex , which keep increasing overtime) to perform liquidations

  • The excess funds become permanently locked in the StabilityPool with no withdrawal mechanism

Tools Used

  • Foundry

  • Manual Review

Recommendations

Remove the second scaling operation in the StabilityPool and rely on the LendingPool's debt calculation:

// StabilityPool.sol
function liquidatePosition(address userAddress) external {
uint256 userDebt = lendingPool.getUserDebt(userAddress);
- uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
- if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
+ if (crvUSDBalance < userDebt) revert InsufficientBalance();
// Continue with liquidation...
}
Updates

Lead Judging Commences

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

Give us feedback!