Core Contracts

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

Broken Liquidation Flow Between StabilityPool and LendingPool

Summary

StabilityPool.sol attempts to bypass the required liquidation sequence by directly calling finalizeLiquidation() on the LendingPool without proper initiation and grace period handling. This makes all liquidation attempts through the StabilityPool fail and renders the liquidation mechanism non-functional.

Vulnerability Details

The vulnerability exists in the StabilityPool's liquidateBorrower() function:

// StabilityPool.sol
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
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();
// Directly calls finalizeLiquidation without proper setup
lendingPool.finalizeLiquidation(userAddress);
}

The LendingPool requires a specific sequence for liquidations:

// LendingPool.sol
function initiateLiquidation(address userAddress) external {
uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();
isUnderLiquidation[userAddress] = true;
liquidationStartTime[userAddress] = block.timestamp;
}
function finalizeLiquidation(address userAddress) external {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
// ... liquidation logic
}

The required sequence is:

  • Check health factor

  • Call initiateLiquidation()

  • Wait for grace period (3 days)

  • Call finalizeLiquidation()
    The StabilityPool attempts to skip steps 1-3, which causes all liquidation attempts to revert.

Impact

All liquidation attempts through StabilityPool will fail because:

  • isUnderLiquidation is never set to true

  • liquidationStartTime is never set

  • Grace period is never respected

  • Health factor is never checked
    This effectively breaks the entire liquidation mechanism of the protocol as:

  • Unhealthy positions cannot be liquidated

  • Protocol's risk management is compromised

  • Bad debt cannot be recovered
    The failure is deterministic and affects 100% of liquidation attempts through the StabilityPool.

Tools Used

  • Manual code review

Recommendations

  • Implement proper liquidation sequence in StabilityPool:

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
// Step 1: Check if liquidation is already initiated
if (!lendingPool.isUnderLiquidation(userAddress)) {
// Check health factor and initiate if needed
uint256 healthFactor = lendingPool.calculateHealthFactor(userAddress);
require(healthFactor < lendingPool.healthFactorLiquidationThreshold(),
"Health factor too high");
lendingPool.initiateLiquidation(userAddress);
emit LiquidationInitiated(userAddress);
return; // Exit and wait for grace period
}
// Step 2: Verify grace period
uint256 startTime = lendingPool.liquidationStartTime(userAddress);
require(block.timestamp > startTime + lendingPool.liquidationGracePeriod(),
"Grace period not expired");
// Step 3: Prepare for liquidation
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();
// Step 4: Execute liquidation
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
Updates

Lead Judging Commences

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