Core Contracts

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

Incorrect Debt Calculation in Stability Pool Liquidation Due to Double Scaling and Stale Index

Relevant Context

The StabilityPool contract is responsible for liquidating borrower positions through its liquidateBorrower() function. The user's debt is obtained from the LendingPool contract, which returns a value that is already scaled by the usage index. The usage index is a critical component that needs to be up-to-date for accurate debt calculations.

Finding Description

There are two interrelated issues in the liquidateBorrower() function that affect debt calculations:

  1. Double Scaling: The user's debt is incorrectly scaled twice:

    • First in LendingPool.getUserDebt(): return user.scaledDebtBalance.rayMul(reserve.usageIndex);

    • Then again in StabilityPool.liquidateBorrower(): uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());

  2. Stale Usage Index: The user's debt is retrieved before updating the lending pool's state:

uint256 userDebt = lendingPool.getUserDebt(userAddress);
// ... later in the code ...
lendingPool.updateState();

Since getUserDebt() uses the current usage index for calculation, using a stale index leads to incorrect debt values. The usage index should be updated before any debt calculations to reflect the most recent interest accrual.

Impact Explanation

High. These issues directly affect the core liquidation functionality of the protocol in two ways:

  1. Double scaling inflates the debt value artificially

  2. Stale usage index results in outdated debt calculations

This combination could prevent legitimate liquidations from being executed due to inflated values failing balance checks. Moreover, this could cause the liquidation to fail because the token approval from StabilityPool is insufficient to cover the user's debt in LendingPool.

Likelihood Explanation

High. Both issues affect every liquidation attempt made through the Stability Pool, making them systematic issues that will consistently produce incorrect results. The impact becomes more significant as the time between usage index updates increases.

Proof of Concept

Consider this scenario:

  1. Last usage index update was 30 days ago at 1.1

  2. User has a scaled debt balance of 100 tokens

  3. Current interest rate would increase usage index to 1.2

  4. Normalized debt is also 1.1

  5. liquidateBorrower() is called

Current implementation:

  1. getUserDebt() uses stale index: 100 * 1.1 = 110 tokens

  2. This is then scaled again: 110 * 1.1 = 121 tokens

  3. updateState() is called after, updating usage index to 1.2

Correct debt should be: 100 * 1.2 = 120 tokens

The actual liquidation attempts to process 121 tokens instead of 120 tokens, which could fail due to insufficient balance checks.

Recommendation

Update the lending pool state before calculating the user's debt and remove the second scaling operation:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
+ lendingPool.updateState();
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();
- bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
+ bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
if (!approveSuccess) revert ApprovalFailed();
- lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
- emit BorrowerLiquidated(userAddress, scaledUserDebt);
+ emit BorrowerLiquidated(userAddress, userDebt);
}
Updates

Lead Judging Commences

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

StabilityPool: liquidateBorrower should call lendingPool.updateState earlier, to ensure the updated usageIndex is used in calculating the scaledUserDebt

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!