Core Contracts

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

liquidations fail even when contract has enough balance

Vulnerability Details

Incorrect Debt Scaling in Liquidation Leads to Failed Liquidations Despite Sufficient Funds

The liquidateBorrower function in the StabilityPool contract erroneously scales the user's debt twice, resulting in an inflated debt value. This causes the protocol to reject valid liquidations when it has sufficient funds to cover the actual debt.

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

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());//@audit
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();// @audit here
LendingPool.sol (getUserDebt Implementation)
function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}
function getNormalizedDebt() external view returns (uint256) {
return reserve.usageIndex;//@audit Same index used in getUserDebt

Impact

Legitimate liquidations are blocked even if the protocol has sufficient crvUSD balance. due to multiplication twice

Undercollateralized positions may remain uncleared, increasing systemic risk.

proof of concept

  1. Setup:

    • User borrows 100 crvUSD (actual debt after scaling = 100 * usageIndex).

    • StabilityPool holds 150 crvUSD.

  2. Trigger Liquidation:

    • getUserDebt() returns 100 * usageIndex.

    • scaledUserDebt becomes (100 * usageIndex) * usageIndex (double scaling).

  3. Check Fails:

    • Compare crvUSDBalance (150) < scaledUserDebt (100 * usageIndex^2).

    • Reverts with InsufficientBalance even though 150 > 100 * usageIndex

Recommendations

Use the debt value directly from getUserDebt():

Updates

Lead Judging Commences

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