Summary
The liquidation mechanism in `LendingPool.sol` relies on a manual trigger via `initiateLiquidation()` to set the `isUnderLiquidation` flag. If this function is not called—even when a user’s health factor falls below the required threshold—the flag remains unset, allowing the user to continue performing sensitive functions such as withdrawing NFTs or borrowing funds.
Vulnerability Details
The `isUnderLiquidation` mapping in the `LendingPool.sol` is only updated in the `initiateLiquidation()` function:
function initiateLiquidation(address userAddress) external nonReentrant whenNotPaused {
if (isUnderLiquidation[userAddress]) revert UserAlreadyUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
UserData storage user = userData[userAddress];
uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();
isUnderLiquidation[userAddress] = true;
liquidationStartTime[userAddress] = block.timestamp;
emit LiquidationInitiated(msg.sender, userAddress);
}
Since this flag is only set when initiateLiquidation() is called, a user who becomes undercollateralized but for whom liquidation is never manually initiated remains marked as not under liquidation. Consequently, functions like withdrawNFT and borrow —which include checks such as:
if (isUnderLiquidation[msg.sender]) revert CannotWithdrawUnderLiquidation();
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
—will continue to allow these actions because the flag remains false.
Proof of Concept
Imagine the following scenario:
User Scenario:
User A deposits collateral and borrows funds. Over time, due to market fluctuations or changes in their collateral value, User A’s health factor falls below the liquidation threshold.
Lack of Trigger:
No party calls initiateLiquidation() on User A—even though their health factor indicates a high risk of default. As a result, isUnderLiquidation[User A] remains false.
Continued Operations:
Since the liquidation flag is not set, User A can still call functions that check if (isUnderLiquidation[msg.sender]) revert .... For example, User A is able to:
Withdraw NFTs: They withdraw NFT collateral even though doing so might further weaken their collateral position.
Borrow More Funds: They can borrow additional reserve assets, worsening their undercollateralized state.
Outcome:
This scenario demonstrates that, without an automatic enforcement mechanism, undercollateralized users can continue risky operations, potentially leading to severe financial imbalances and systemic risk.
Impact
Risk Exposure: Undercollateralized users can continue to withdraw collateral or borrow additional funds even when they should be restricted.
Bypass of Safety Mechanisms: The intended safeguard to prevent risky operations during liquidation is effectively bypassed if the manual process is not triggered.
Increased Systemic Risk: Permitting operations for undercollateralized users can exacerbate financial instability within the protocol, potentially leading to larger losses or defaults.
Tools Used
Manual Review and Foundry
Recommendations
Automatic Liquidation Enforcement:
Implement a mechanism that automatically updates the liquidation status (i.e., isUnderLiquidation) based on the user's health factor. This check should occur either periodically or at the beginning of sensitive operations.
Inline Health Factor Check:
In functions like `withdrawNFT` and `borrow`, include a direct check of the user’s health factor in addition to relying on the isUnderLiquidation flag. For example, if the health factor is below the threshold, the operation should revert regardless of the flag’s state.
Automated Monitoring and Alerts:
Develop off-chain monitoring tools that trigger liquidation processes when a user’s health factor falls below the threshold, ensuring timely intervention.
By automating the enforcement of liquidation conditions, the protocol can better protect itself from undercollateralized positions and reduce the risk of further financial instability.