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 liquidateBorrower::StabilityPool Function Due to Redundant reserve.usageIndex Application

Finding Description and Impact

In the liquidateBorrower function, the user's debt is scaled twice using the same reserve.usageIndex:

  1. First, userDebt is calculated as user.scaledDebtBalance.rayMul(reserve.usageIndex) in the getUserDebt function.

  2. Then, scaledUserDebt is calculated as WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()), where getNormalizedDebt() also returns reserve.usageIndex.

This double scaling results in the user's debt being multiplied by reserve.usageIndex twice, leading to:

  • Overestimation of the user's debt.

  • Incorrect liquidation amounts, potentially causing financial harm to borrowers or the Stability Pool.


Proof of Concept

  1. liquidateBorrower in StabilityPool:

    function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
    _update();
    uint256 userDebt = lendingPool.getUserDebt(userAddress); // First scaling: user.scaledDebtBalance.rayMul(reserve.usageIndex)
    uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt()); // Second scaling: rayMul(userDebt, reserve.usageIndex)
    ...
    }
  2. getUserDebt in LendingPool:

    function getUserDebt(address userAddress) public view returns (uint256) {
    UserData storage user = userData[userAddress];
    return user.scaledDebtBalance.rayMul(reserve.usageIndex);
    }
  3. getNormalizedDebt in LendingPool:

    function getNormalizedDebt() external view returns (uint256) {
    return reserve.usageIndex;
    }

Explanation

  • The getUserDebt function already scales the user's debt by reserve.usageIndex.

  • The liquidateBorrower function then scales the already scaled userDebt by reserve.usageIndex again, effectively applying the scaling twice.

Example Scenario

Assume:

  • user.scaledDebtBalance = 100e18

  • reserve.usageIndex = 1.1e27 (10% interest)

  1. First Scaling (in getUserDebt):

    userDebt = 100e18 * 1.1e27 / 1e27 = 110e18;
  2. Second Scaling (in liquidateBorrower):

    scaledUserDebt = 110e18 * 1.1e27 / 1e27 = 121e18;

The user's debt is incorrectly calculated as 121e18 instead of the correct 110e18. This overestimation could lead to excessive liquidation, causing financial harm to the borrower.


Recommended Mitigation Steps

To fix this issue, remove the second scaling step in the liquidateBorrower function. The corrected function should look like this:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress); // Already scaled by reserve.usageIndex
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < userDebt) revert InsufficientBalance(); // Ensure Stability Pool has enough funds
// Approve the LendingPool to transfer the debt amount
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, userDebt);
}

Explanation of Fix

  • The second scaling step (WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt())) is removed.

  • The userDebt value returned by lendingPool.getUserDebt(userAddress) is already correctly scaled by reserve.usageIndex and does not need further adjustment.

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.