Core Contracts

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

Double Scaling of User Debt in StabilityPool Prevents Valid Liquidations

Link to Affected Code:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/StabilityPool.sol#L449-L471

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
uint256 userDebt = lendingPool.getUserDebt(userAddress); // Already scaled by index
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()); // Double scaling
if (userDebt == 0) revert InvalidAmount();
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance(); // Checks against inflated amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
}

Description:
The StabilityPool's liquidateBorrower function incorrectly scales the user's debt twice(Index**2), causing valid liquidations to fail at the balance check stage. The issue occurs because:

  1. lendingPool.getUserDebt() returns debt already scaled by the normalized debt index

  2. The function then applies rayMul with getNormalizedDebt() again

  3. This double-scaled amount is used for the balance check if (crvUSDBalance < scaledUserDebt)

  4. Even if the pool has enough funds to cover the actual debt, it will revert due to checking against an inflated amount

Impact:

  1. Valid liquidations to fail at the balance check stage

  2. StabilityPool needs index² times more funds than actually required

  3. System cannot liquidate positions even with sufficient funds

  4. Bad Debts would accumulate

Proof of Concept:

  • Lets look a scenerio.
    Using WadRayMath's rayMul implementation:

// Given:
baseDebt = 1000e18
index = 1.4e27 (40% interest accrued)
StabilityPool balance = 1500e18
// Step 1: In LendingPool.getUserDebt():
userDebt = rayMul(baseDebt, index)
= (1000e18 * 1.4e27 + 0.5e27) / 1e27
= 1400e18 // Correctly scaled once
// Step 2: In StabilityPool.liquidateBorrower():
scaledUserDebt = rayMul(userDebt, index)
= (1400e18 * 1.4e27 + 0.5e27) / 1e27
= 1960e18 // Double scaled
// Step 3: Balance check in StabilityPool:
if (1500e18 < 1960e18) revert InsufficientBalance()
// Reverts because 1500e18 < 1960e18
// Even though 1500e18 is MORE than enough to cover actual debt of 1400e18

Recommended Mitigation:
Remove the second scaling since getUserDebt() already returns the normalized amount:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
uint256 userDebt = lendingPool.getUserDebt(userAddress); // Already normalized
if (userDebt == 0) revert InvalidAmount();
if (crvUSDBalance < userDebt) revert InsufficientBalance();
bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, userDebt);
}
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!