Core Contracts

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

Liquidability thresholds are different across the system

Summary

The system uses 2 different thresholds to determine if a user is healthy.

Vulnerability Details

The system defines liquidatabale in 2 different ways:

  • 1️⃣ If a user's collateral is worth less than 80% of their debt. (80% == BASE_LIQUIDATION_THRESHOLD == liquidationThreshold, by default is set to 80%)

  • 2️⃣ If a user's health factors is less than 1e18. (BASE_HEALTH_FACTOR_LIQUIDATION_THRESHOLD == 1e18, by default even though can be changed)

These 2 are different because the way the health factor is calculated has nothing to due with the collateral value being worth less than 80% of the debt.

Let's see how the heath factor is calculated here:

function calculateHealthFactor(address userAddress) public view returns (uint256) {
uint256 collateralValue = getUserCollateralValue(userAddress);
uint256 userDebt = getUserDebt(userAddress);
if (userDebt < 1) return type(uint256).max;
@> // 👁️ Calculating 80% of the user's collateral value 👁️
@> uint256 collateralThreshold = collateralValue.percentMul(liquidationThreshold);
@> // 👁️ Returning the health, effectively checking if: 80% of the collateral is more than 100% of the debt 👁️
@> // Instead of checking if 100% of the collateral is more than 80% of the debt
return (collateralThreshold * 1e18) / userDebt;
}
@> // in initiateLiquidtation() the following check is made, health above 1e18 is not liquidatable
@> // See here https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/pools/LendingPool/LendingPool.sol#L457
@> if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();

Now in borrow() we see that the check to avoid users becoming instantly liquidatable is done like so:

// Ensure the user has enough collateral to cover the new debt
@> // 👁️ Checking if 100% of collateral is less than 80% of debt.
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}

Numerical Example

These are different checks providing different results:

  • Debt value: 100.

  • Collateral value: 110.

  • 1️⃣ 80% of the debt is 80. The collateral is worth more than 80% of the debt -> Not liquidatable.

  • 2️⃣ The health factor is 0.8*110 / 100 = 0.88. The health factor is less than 1 -> Liquidatable.

Impact

Some users will be liquidatable according to the logic in some functions and not liquditable according to the logic in other functions.

Recommendations

Choose one and stick to it. I would recommend the one in calculateHealthFactor() as it is a more conservative approach to avoid bad debt.

Updates

Lead Judging Commences

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

LendingPool::borrow as well as withdrawNFT() reverses collateralization check, comparing collateral < debt*0.8 instead of collateral*0.8 > debt, allowing 125% borrowing vs intended 80%

Support

FAQs

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