Core Contracts

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

LendingPool::finalizeLiquidation doesn't check user's healthFactor; borrowers may be liquidated unfairly

Summary

Liquidation process in LendingPool consists in 2 steps: initiateLiquidation where a liquidationStartTime counter is started and, after liquidationGracePeriod has expired, finalizeLiquidation can be called to settle the liquidation.
Liquidation can be started if collatera-to-debt ratio, named healthFactor, falls under a specific value.
healthFactor can oscilate due to 2 reasons: collateral value changes or debt accumulates interest, increasing the borrower's debt.

//LendingPool.sol
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;
}

After grace period expires, LendingPool::finalizeLiquidation can be called.
This function executes the following :
-borrower's NFT are transferred to stabilityPool,

  • borrower's debt is burned and his data is updated;

  • reserve assets are transferred from stabilityPool to lendingPool to cover for liquidated debt position.

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 problem is finalizeLiquidation doesn't check for borrower's healthFactor which could have improved for several reasons:

  • borrower's debt was partially or fully repaid via repay or repayOnBehalf;

  • collateral asset value increased.
    Even if healthFactor is considered healthy, borrowers are still liquidatable.

Vulnerability Details

Borrowers may be liquidated unfairly.

Impact

Tools Used

Recommendations

Update finalizeLiquidation to check for borrower's health factor. Proceed with liquidation only if health factor is unhealthy.

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.