Core Contracts

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

Critical Inverted Liquidation Check in the LendingPool Contract

Summary

The LendingPool contract contains a critical logical error in its liquidation mechanism. Specifically, the initiateLiquidation function reverts whenever the healthFactor is greater than or equal to the configured threshold, inverting the expected behavior. As a result, healthy positions (those with a sufficiently high healthFactor) are incorrectly treated as undercollateralized, while positions that genuinely require liquidation may not be handled properly. This misalignment undermines the protocol’s core risk management, allowing or preventing liquidations under the wrong conditions.

Vulnerability Details

Within the initiateLiquidation function, the contract checks whether a user’s healthFactor is greater than or equal to the healthFactorLiquidationThreshold and then reverts with HealthFactorTooLow(). This condition is inverted, as a higher or equal healthFactor typically indicates a safer (more solvent) position rather than one that should be liquidated. Consequently, healthy positions are incorrectly deemed undercollateralized, and genuinely unsafe positions may remain exempt from liquidation. This flawed check disrupts the protocol’s core risk management logic by enabling or disallowing liquidations based on the wrong criteria.

Impact

The inverted check in the initiateLiquidation function undermines the protocol’s core risk management. Healthy positions, which should remain exempt from liquidation, can be improperly flagged as undercollateralized, forcing unnecessary liquidations. Meanwhile, genuinely insolvent borrowers may avoid liquidation. This deviation from the intended collateral management logic jeopardizes the overall stability of the system, leading to potential defaults, loss of confidence, and financial damage for the protocol and its users.

Proof of Concept

The contract uses the following logic in the initiateLiquidation function to determine if a user is eligible for liquidation:

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

The expected behavior is to prevent liquidation if the healthFactor is above (or equal to) the threshold, because a high health factor normally indicates solvency. However, the revert message (HealthFactorTooLow) suggests the opposite condition, effectively reversing the intended logic. Consequently, a user with a sufficiently high (healthy) healthFactor would be inappropriately subject to liquidation.

Code Analysis

Focusing on the relevant portion:

function initiateLiquidation(address userAddress) external nonReentrant whenNotPaused {
// ... Preliminary code ...
// (!) Inverted logic when comparing healthFactor and healthFactorLiquidationThreshold
uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();
// ^ This implies the user is “healthy” and should not be liquidated
// ... Subsequent code ...
}

( ! ) Correct liquidation logic should require healthFactor < healthFactorLiquidationThreshold to proceed.

Explanation

The problem lies in the condition if (healthFactor >= healthFactorLiquidationThreshold) triggering a revert with a message “HealthFactorTooLow.” In standard lending protocols, a user is subject to liquidation when their health factor is below a critical threshold (e.g., < 1). If the healthFactor is greater or equal to the threshold, the user should not be liquidated because their collateral is sufficient. Here, however, the contract does the opposite, reverting for solvent users and allowing liquidation only if healthFactor < healthFactorLiquidationThreshold. This reverses the typical lending logic and can lead to incorrect or inconsistent liquidation events.

Vulnerable Scenario

  • A user has a health factor of 2.0, well above the liquidation zone.

  • The liquidation threshold (healthFactorLiquidationThreshold) is set to 1.0.

  • Calling initiateLiquidation(userAddress) causes the condition healthFactor >= 1.0 to trigger a revert with HealthFactorTooLow. This is contradictory, as that user should not be considered undercollateralized.

As a result, the comparison is inverted, potentially disallowing liquidations that should happen or confusingly allowing them under incorrect conditions.

Test and Result

In this test, the user has a high Health Factor (well above the liquidation threshold), yet the contract still reverts with HealthFactorTooLow. This confirms that the liquidation logic is inverted: instead of allowing well-collateralized positions to remain safe, the contract incorrectly treats them as undercollateralized.

  • Add the following test inside the describe("Liquidation", function () {}) block in test/unit/core/pools/LendingPool/LendingPool.test.js.

it("should (incorrectly) revert liquidation when user is well collateralized (showcasing the inverted logical error)", async function () {
// Verify that the user previously borrowed 80 crvUSD, backed by an NFT.
const userDebt = await lendingPool.getUserDebt(user1.address);
expect(userDebt).to.equal(ethers.parseEther("80"));
// Raise the NFT price significantly (e.g., 500 crvUSD) to ensure the user’s collateral is well above the debt.
await raacHousePrices.setHousePrice(1, ethers.parseEther("500"));
// Confirm that the user’s Health Factor is now greater than the liquidation threshold, indicating strong collateralization.
const healthFactor = await lendingPool.calculateHealthFactor(user1.address);
const threshold = await lendingPool.healthFactorLiquidationThreshold();
expect(healthFactor).to.be.gt(threshold);
// Initiate liquidation from a different account (user2), expecting an incorrect revert due to the inverted condition.
await expect(lendingPool.connect(user2).initiateLiquidation(user1.address))
// Despite having a high Health Factor, the contract reverts with "HealthFactorTooLow", revealing the reversed logic.
// Because of the inverted logic, we see "HealthFactorTooLow" even though HF is high
.to.be.revertedWithCustomError(lendingPool, "HealthFactorTooLow");
// This outcome confirms the condition is reversed: ordinarily, a revert should occur only when HF < threshold,
// but here it triggers if HF >= threshold.
});
LendingPool
Liquidation
✔ should (incorrectly) revert liquidation when user is well collateralized (showcasing the inverted logical error)

Confirmation

  • The revert message (HealthFactorTooLow()) does not align with the condition (healthFactor >= healthFactorLiquidationThreshold).

  • Logically, the system should revert if healthFactor is below the threshold, not if it is above or equal.

This confirms a logic error that may undermine the security and correctness of the liquidation process.

Tools Used

Manual Code Review

A thorough, line-by-line inspection of the contract’s code was performed, focusing on the initiateLiquidation function and related liquidation logic to uncover the inverted condition.

Recommendations

Use a correct comparison in the initiateLiquidation function to align with typical lending protocols. Specifically, revert when the healthFactor is below the threshold, rather than above. Adjust the revert message accordingly to match the intended logic:

- if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();
+ if (healthFactor < healthFactorLiquidationThreshold) revert HealthFactorTooLow();
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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