Core Contracts

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

Users' NFT Collateral Could Be Stuck in Liquidation Without the Ability to Withdraw

Summary

The current protocol design limits users’ ability to withdraw their collateral (NFTs) if they are under liquidation. If an external issue (e.g., website issue, network issue, protocol pause) prevents users from repaying debt within the liquidationGracePeriod, users cannot close their liquidation. Additionally, even after a debt is repaid beyond the grace period, users cannot withdraw their NFT collateral because the liquidation status is still active. This leaves the user in a state where their collateral is locked.

Vulnerability Details

This issue arises from the interaction between the functions that handle liquidation (closeLiquidation) and NFT withdrawal (withdrawNFT) when the protocol is paused or a network issue delays repayment past the grace period.

Example Scenario:

1- Bob's account is flagged as isUnderLiquidation = true, and liquidationStartTime[userAddress] is set. He plans to repay before liquidationStartTime[userAddress] + liquidationGracePeriod expires.

2- A website issue, network issue or protocol pause delays his repayment. Bob repays the debt after the service is up, since the grace period has expired, Bob cannot call closeLiquidation(), meaning his liquidation status does not reset.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/pools/LendingPool/LendingPool.sol#L476

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
@> if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}

3 - The manager or owner tries to liquidateBorrower. LiquidateBorrower() requires userDebt > 0 to proceed, but Bob has already repaid his debt, so userDebt == 0 (assuming no bug to cause dust), causing the function to revert. Because liquidateBorrower() never reaches lendingPool.finalizeLiquidation(userAddress);, Bob’s liquidation status remains stuck (isUnderLiquidation = true).

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/pools/StabilityPool/StabilityPool.sol#L449-L467

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());
@> if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/pools/LendingPool/LendingPool.sol#L511-L512

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;

4 - Bob is not able to withdraw NFT even if he repays all the debt, since his isUnderLiquidation[msg.sender] is still true
https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/pools/LendingPool/LendingPool.sol#L289

function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
if (isUnderLiquidation[msg.sender]) revert CannotWithdrawUnderLiquidation();

Impact

  1. User Funds Are Stuck: Bob has no way to reclaim his NFT after full repayment.

  2. No Administrative Workaround: Even the owner/manager cannot manually reset liquidation status in this scenario.

Tools Used

Manual code review

Recommended Mitigation

  1. Admin workaround ensures manual recovery in extreme cases.

  2. Reopening closeLiquidation() allows automatic recovery for users repaying late after accidents.

Updates

Lead Judging Commences

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

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.

Give us feedback!