Core Contracts

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

Double Interest Calculation in liquidateBorrower() Function Leads to Failed Liquidations and Protocol Risk

Summary

A vulnerability was discovered in the liquidateBorrower() function that could prevent the liquidation of a borrower even when the pool holds sufficient funds to cover their debt. The issue stems from the incorrect calculation of scaledUserDebt, which applies interest twice, resulting in an inflated debt value.

Affected Code: StabilityPool::liquidateBorrower


Vulnerability Details

The issue occurs in the following line within the liquidateBorrower() function:

uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
  • getUserDebt() already returns the user's actual debt with the interest, which is computed as:

    userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);

    Here, usageIndex represents accumulated interest.

  • getNormalizedDebt() returns the current usageIndex.

  • Multiplying userDebt by getNormalizedDebt() applies interest twice:

    scaledUserDebt = userDebt * liquidityIndex; // Double interest applied

The double interest calculation is happening in this line:

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

The issue arises because userDebt already includes interest, and multiplying it by getNormalizedDebt() applies the interest again.

Example Scenario Illustration

1. User Borrowing:

  • The user borrows 10 units of crvUSD.

  • At the time of borrowing:

    • reserve.usageIndex = 1e27

    • reserve.liquidityIndex = 1e27

  • Over time:

    • reserve.usageIndex increases to 1.5e27.

    • reserve.liquidityIndex increases to 1.3e27.

2. getUserDebt() Calculation:

  • Formula: getUserDebt = scaledDebtBalance × current usageIndex

  • Compute scaledDebtBalance:

    • scaledDebtBalance = borrowed amount ÷ usageIndex at borrow time

    • scaledDebtBalance = 10 ÷ 1e27 = 10e-27

  • Compute getUserDebt:

    • getUserDebt = 10e-27 × 1.5e27 = 15

  • getUserDebt() returns 15 units, reflecting accrued interest.

3. scaledUserDebt Calculation (Double Interest Issue):

  • Formula (incorrect): scaledUserDebt = getUserDebt × liquidityIndex

  • Compute scaledUserDebt:

    • scaledUserDebt = 15 × 1.3e27 = 19.5e27

  • Interest is applied twice, inflating the debt to 19.5 instead of the correct 15.

Impact

  • Failed Liquidation: The system will attempt to liquidate the borrower using an inflated debt amount (scaledUserDebt), which could exceed the pool's actual balance.

  • Loss of Funds: Liquidation will revert with an InsufficientBalance error, even if the pool has enough to cover the borrower's real debt.


Tools Used

  • Manual code review


Recommendations

Remove the calculation that scales user debt by more interest.

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();
// Approve the LendingPool to transfer the debt amount
// @ audit user may not be liquidated even if pool has enough funds to cover his debt as scaledUserDebt is too high
- bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
+ bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
- emit BorrowerLiquidated(userAddress, scaledUserDebt);
+ emit BorrowerLiquidated(userAddress, userDebt);
}
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.