Core Contracts

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

Users under liquidation but have paid their debt can be liquidated

Summary

The liquidation process does not properly verify whether a user still has debt before proceeding with finalizeLiquidation(). If a user repays their debt during the grace period but does not call closeLiquidation(), the contract will still treat them as under liquidation, resulting in the loss of all their NFTs when liquidation is finalized.

Vulnerability Details

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
// Transfer NFTs to Stability Pool
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// Update user's scaled debt balance
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}

The finalizeLiquidation() function checks only if the grace period has expired, but does not verify whether the user still has outstanding debt before proceeding with liquidation.

If a user repays their entire debt during the grace period but does not explicitly call closeLiquidation(), the system still considers them under liquidation.

As a result, all their NFTs will be transferred to the Stability Pool, even though they have no debt.

Impact

Users who repaid their debt will still lose all their NFTs when liquidated.

Users with no debt left should not be liquidated, yet they are.

POC

Step 1: User Takes a Loan

  • User deposits NFTs as collateral.

  • User borrows stablecoins against their NFTs.

  • Step 2: Liquidation is Initiated

    • The health factor drops below the threshold, triggering initiateLiquidation().

    • The user is marked as isUnderLiquidation = true, and the grace period begins.

  • Step 3: User Repays Debt

    • The user repays all their outstanding debt during the grace period.

    • However, the user does not call closeLiquidation().

  • Step 4: Liquidation is Finalized

    • After the grace period expires, finalizeLiquidation() is called.

    • The function does not check if the user’s debt is now zero.

    • The NFTs are transferred to the Stability Pool even though the user no longer owes any debt.

Tools Used

Manual Review

Recommendations

Verify Debt Before Liquidation

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.