Core Contracts

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

Incorrect Liquidation Exit Condition Forces Liquidate Healthy Positions

Summary

The LendingPool.closeLiquidation() function incorrectly requires users to repay their entire debt (down to dust threshold) to exit liquidation, instead of checking if their position has become healthy again. This prevents users from exiting liquidation through partial repayments or additional collateral deposits, forcing unnecessary liquidations even when positions could be solvent.

Vulnerability Details

The issue occurs in closeLiquidation() where the function enforces full debt repayment:

function closeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
// @audit-issue: Forces full debt repayment instead of checking health factor
>> if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
}

The core issue is that the protocol requires debt to be essentially zero (< DUST_THRESHOLD) to exit liquidation. This is fundamentally wrong because:

  1. Users can make their position healthy again through multiple means:

    • Partial debt repayment

    • Additional collateral deposit

    • Combination of both

  • This works against the protocol, as in periods of volatility, most users would need to repay all their debt, which forces them to exit the protocol instead of making their position healthy again

  • The protocol's health factor calculation is designed to determine position safety:

function calculateHealthFactor(address userAddress) public view returns (uint256) {
uint256 collateralValue = getUserCollateralValue(userAddress);
uint256 userDebt = getUserDebt(userAddress);
uint256 collateralThreshold = collateralValue.percentMul(liquidationThreshold);
return (collateralThreshold * 1e18) / userDebt;
}
  • However, it's not used to determine if users can exit liquidation or not, which leads to users who restore their position to a healthy state not being able to exit liquidation and getting liquidated even with a healthy position

Impact

  • Users are forced into full liquidation even when they restore position health through partial repayment or additional collateral

  • Protocol loses healthy positions that could have been restored, leading to unnecessary bad debt

Tools Used

  • Manual Review

  • Foundry

Recommendations

Replace debt check with health factor check in closeLiquidation():

function closeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
UserData storage user = userData[userAddress];
- uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
- if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
+ uint256 healthFactor = calculateHealthFactor(userAddress);
+ if (healthFactor < healthFactorLiquidationThreshold) revert UnhealthyPosition();
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
}
Updates

Lead Judging Commences

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

LendingPool::finalizeLiquidation() never checks if debt is still unhealthy

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.