Core Contracts

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

Failure to Close Liquidation Due to Edge Case Timing Issues

Summary

The LendingPool contract requires borrowers to manually call closeLiquidation after repaying their debt during the liquidation grace period. However, an edge case arises when the borrower repays their debt at the very end of the grace period but does not get the chance to call closeLiquidation before the grace period expires. This results in an unintended liquidation, even though the borrower has fully repaid their debt.

Vulnerability Details

Issue Explanation

Manual Closure of Liquidation is Required

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
emit LiquidationClosed(userAddress);
}

The function enforces a strict grace period check:

if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}

If the borrower repays at the last moment of the grace period, the grace period can expire before they manage to call closeLiquidation(). This results in an unfair liquidation, even though the borrower has fully repaid.

Edge Case Scenario Leading to the Issue

  1. User is flagged for liquidation and given a grace period to repay the debt.

  2. User repays the full debt at the very last moment of the grace period.

  3. Grace period expires immediately after repayment, preventing the borrower from calling closeLiquidation().

  4. Since closeLiquidation() cannot be called after grace period expiration, the borrower remains flagged for liquidation despite having repaid.

  5. The borrower is unfairly liquidated, as they never got a chance to remove the liquidation flag.

Impact

  • Unfair Liquidations Borrowers who successfully repay their debt may still be liquidated due to timing constraints.

  • Loss of Collateral Even though the borrower cleared their debt, they are wrongfully subjected to forced liquidation.

Recommendations

Solution 1 Automatically Call closeLiquidation in repay
Modify repay to automatically call closeLiquidation after full debt repayment, avoiding the edge case.

function repay(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
_repay(amount, msg.sender);
// Edge Case Fix Automatically close liquidation if debt is cleared
if (userData[msg.sender].scaledDebtBalance.rayMul(reserve.usageIndex) <= DUST_THRESHOLD) {
closeLiquidation();
}
}

Solution 2 Remove Grace Period Check in closeLiquidation if Debt is Zero
Modify closeLiquidation() so that if the user has repaid their debt, the function can still be executed even after the grace period expires.

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// Edge Case Fix Allow closing liquidation if debt is zero, regardless of grace period
if (userData[userAddress].scaledDebtBalance.rayMul(reserve.usageIndex) <= DUST_THRESHOLD) {
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
emit LiquidationClosed(userAddress);
return;
}
if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}
}

Solution 3 Add Health Factor Check in closeLiquidation
Instead of enforcing a strict grace period check, validate the borrower's health factor before finalizing liquidation.

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
uint256 healthFactor = getHealthFactor(userAddress);
if (healthFactor >= SAFE_THRESHOLD) { // Ensure borrower is solvent
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
emit LiquidationClosed(userAddress);
return;
}
if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}
}

Updates

Lead Judging Commences

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

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

A borrower can LendingPool::repay to avoid liquidation but might not be able to call LendingPool::closeLiquidation successfully due to grace period check, loses both funds and collateral

Support

FAQs

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