Core Contracts

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

Borrower can still be liquidated even when they have already paid their debt

Summary

In LendingPool.sol, once a borrower has had initiateLiquidation()called on their address, they can repay their debt within the grace period to avoid being liquidated. However, if borrower had repaid their debt but did not call closeLiquidation() in time, they will still get liquidated.

Vulnerability Details

In closeLiquidation(), the function checks if the block.timestampis within the start time + grace period. If it is beyond that, revert.

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();

Assume this scenario:

  • liquidationStartTime[Alice's address] = 1708423200

  • liquidationStartTime[Alice's address] + liquidationGracePeriod = 1708682400

  • Alice repays her debt fully at 1708681800

  • Alice only calls closeLiquidation()at 1708682401, which is beyond 1708682400

  • closeLiquidation()will revert

Now, Stability Pool can finalize the liquidation via finalizeLiquidation().Focusing on the snippet of the function below:

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);
  • Since Alice had repaid her debt, assume that userDebt = 0.

  • Even when Alice does not have any more debt, all of her NFTs are still being transferred to Stability Pool

  • finalizeLiquidation()function does not check for Alice's actual debt. Alice will lose her collateral even when she has no debt.

Impact

There is no check to ensure that user indeed has remaining debt before liquidating. This leads to unfair losses for borrowers. For example, if a borrower's debt is much smaller than their total NFT value, borrowers will lose all of their NFTs.

Tools Used

Manual

Recommendations

  1. In closeLiquidation(), store and check the timestamp of last repayment within the grace period, instead of checking against the current block.timestamp.

  2. In finalizeLiquidation(), additional logic can be added to only liquidate based on the amount of remaining debt, instead of liquidating all of the borrower's NFTs.

Updates

Lead Judging Commences

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

Give us feedback!