Core Contracts

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

Borrowers can lose their RaacNFT when improving their health factor

Summary

Borrowers enter liquidation when they fall below a certain health factor threshold, however improving their health is not accounted for once the liquidation starts.

Details

Liquidators can start liquidations via initiateLiquidation which determines whether a user should be liquidated based on a health factor calculation

uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();

The health factor is determined by dividing the value of all user deposited NFTs and their debt+interest with an adjustment for liquidationThreshold

function calculateHealthFactor(address userAddress) public view returns (uint256) {
uint256 collateralValue = getUserCollateralValue(userAddress);
uint256 userDebt = getUserDebt(userAddress);
if (userDebt < 1) return type(uint256).max;
uint256 collateralThreshold = collateralValue.percentMul(liquidationThreshold);
return (collateralThreshold * 1e18) / userDebt;
}

Users can maintain a healthy position by either depositing more NFTs or partially repaying their debt. However once liquidation starts, improving their health doesn't allow them to stop the liquidation, only a full repayment can. Liquidation can only be stopped by calling closeLiquidation, unfortunately, no health checks are performed there.

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
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;
}

Users who partially repaid their debt or deposited more collateral won't be able to close the liquidation and their funds are practically lost.
Additionally this opens up potential timing issues such as:

  1. Alice sees they are eligible for liquidation and decide to deposit more collateral and repay a small amount to improve health

  2. Bob sees the transaction in the mempool and front-run calls initiateLiquidation

  3. isUnderLiquidation[Alice] = true, however Alice's transaction goes through

  4. Alice can't close the liquidation despite having improved their health.

  5. Alice can't withdraw her NFT due to being under liquidation, her partial repayment is neglected too

  6. Grace period passes, Alice is liquidated and all NFTs are seized

Sufficiently collaterized user was liquidated, more NFTs were liquidated than they should've.

Impact

Logic error, missing functionality, unfair liquidations.

Mitigation

Either add a health factor check in closeLiquidation same as in initiateLiquidation or disallow NFT deposits during liquidation as they do nothing to improve user's health.

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

Support

FAQs

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

Give us feedback!