Core Contracts

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

Incorrect Dust Threshold Comparison in closeLiquidation causes a potential DoS

Summary

The closeLiquidation function in the LendingPool contract is intended to allow a user to close their liquidation status by repaying their outstanding debt during the grace period. However, the function’s validation for the remaining debt uses the wrong comparison operator relative to the DUST_THRESHOLD. Currently, the code reverts if the user's remaining debt is greater than the DUST_THRESHOLD (set to 1e6). According to the intended behavior, the function should only allow the closure of liquidation when the user's remaining debt is significant (i.e. greater than or equal to 1e6), and it should revert if the user's remaining debt is less than this threshold. In other words, the check should revert if userDebt < DUST_THRESHOLD rather than if userDebt > DUST_THRESHOLD. This bug may allow users with negligible debt to bypass the liquidation process, leading to incorrect system behavior and potential denial of service.

Vulnerability Details

How It Begins

  1. Current Behavior:

    In the closeLiquidation function, after updating the reserve state and verifying that the grace period has not expired, the function calculates the user's debt:

    uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
    if (userDebt > DUST_THRESHOLD) revert DebtNotZero();

    With the current check, if a user's remaining debt is greater than the DUST_THRESHOLD (1e6), the function reverts with DebtNotZero(). This prevents liquidation closure for users who still have a significant debt, contrary to the intended logic.

  2. Intended Behavior:

    The function is expected to allow closing liquidation when a user has a significant remaining debt—specifically, when userDebt is greater than or equal to 1e6. If the user’s remaining debt is below 1e6 (i.e. considered negligible or "dust"), then the function should revert. The correct check should thus be:

    if (userDebt < DUST_THRESHOLD) revert DebtNotZero();

    This change ensures that liquidation can only be closed if the user's remaining debt is at least 1e6, preventing scenarios where users might bypass the liquidation process with an insufficient repayment.

Proof of Concept

Scenario Example

  1. Setup:

    • Alice deposits 1000e18 reserve assets and mints an NFT as collateral.

    • She borrows 900e18, reducing her health factor below the liquidation threshold.

  2. Liquidation Initiation:

    • Bob initiates Alice's liquidation, flagging her as under liquidation and starting the grace period.

  3. Liquidation Closure Attempt:

    • When Alice calls closeLiquidation, the function calculates her remaining debt.

    • With the current check (if (userDebt > DUST_THRESHOLD) revert DebtNotZero();), if Alice’s remaining debt is greater than 1e6, the function reverts—blocking her from closing liquidation even when she has repaid enough to be eligible.

    • According to the intended logic, the function should allow closure when the remaining debt is significant (≥1e6) and only revert when it is too low (i.e., <1e6). The improper check leads to a denial of liquidation closure.

Test Suite Code

function testUsersAreNotAbleToCloseLiquidationDueToDoSCausedByWrongDustThresholCheck() public {
uint256 tokenId = 1;
uint256 housePrice = 1000e18;
vm.startPrank(RAAC_HOUSE_PRICES_OWNER);
// set oracle
raacHousePrices.setOracle(RAAC_HOUSE_PRICES_ORACLE);
vm.stopPrank();
vm.startPrank(RAAC_HOUSE_PRICES_ORACLE);
// house price in USD
raacHousePrices.setHousePrice(tokenId, housePrice);
vm.stopPrank();
// ensure alice has sufficient balance
crvUsdToken.mint(ALICE, 1000e18);
vm.startPrank(ALICE);
crvUsdToken.approve(address(lendingPool), 1000e18);
// deposit 1000e18 into lending pool to have some liquidity
lendingPool.deposit(1000e18);
vm.stopPrank();
// alice pays in erc20 to mint nft
erc20Mock.mint(ALICE, housePrice);
vm.startPrank(ALICE);
erc20Mock.approve(address(raacNft), housePrice);
// mint 1 nft with tokenId 1
raacNft.mint(tokenId, housePrice);
vm.stopPrank();
vm.startPrank(ALICE);
raacNft.approve(address(lendingPool), tokenId);
// deposit nft into lending pool
lendingPool.depositNFT(tokenId);
vm.stopPrank();
(, uint256 scaledDebtBalance,, bool isUnderLiquidation,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After depositNFT... ");
console.log("scaledDebtBalance: ", scaledDebtBalance);
console.log("isUnderLiquidation: ", isUnderLiquidation);
uint256 borrowAmount = 900e18;
vm.startPrank(ALICE);
// borrow 900e18
lendingPool.borrow(borrowAmount);
vm.stopPrank();
(, uint256 scaledDebtAfterBorrow,,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After borrow... ");
console.log("scaledDebtBalance: ", scaledDebtAfterBorrow);
console.log("isUnderLiquidation: ", isUnderLiquidation);
// Ensure Alice's health factor is below threshold to allow liquidation initiation
uint256 aliceHealthFactor = lendingPool.calculateHealthFactor(ALICE);
assert(aliceHealthFactor < lendingPool.BASE_HEALTH_FACTOR_LIQUIDATION_THRESHOLD());
// Bob initiates Alice's liquidation
vm.startPrank(BOB);
lendingPool.initiateLiquidation(ALICE);
vm.stopPrank();
(, uint256 scaledDebtAfterLiquidation,, isUnderLiquidation,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After initiate liquidation... ");
console.log("scaledDebtBalance: ", scaledDebtAfterLiquidation);
console.log("isUnderLiquidation: ", isUnderLiquidation);
vm.startPrank(ALICE);
vm.expectRevert(bytes4(keccak256("DebtNotZero()")));
lendingPool.closeLiquidation();
vm.stopPrank();
}

How to Run the Test

  1. Create a Foundry Project:
    Open your terminal and run:

    forge init my-foundry-project
  2. Place Contract Files:
    Place all relevant contract files (e.g., LendingPool.sol, RAACNFT.sol, RAACHousePrices.sol, etc.) in the src directory of your project.

  3. Create Test Directory:
    Create a directory named test adjacent to the src directory, and add the test file (e.g., PoolsTest.t.sol) containing the above test suite code.

  4. Run the Test:
    Execute the following command in your terminal:

    forge test --mt testUsersAreNotAbleToCloseLiquidationDueToDoSCausedByWrongDustThresholCheck -vv

    This command will run the specific test with verbose output, allowing you to verify that the current dust threshold check is causing a revert when it should allow liquidation closure for users with significant remaining debt.

  5. Output:

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/PoolsTest.t.sol:PoolsTest
[PASS] testUsersAreNotAbleToCloseLiquidationDueToDoSCausedByWrongDustThresholCheck() (gas: 1048280)
Logs:
After depositNFT...
scaledDebtBalance: 0
isUnderLiquidation: false
After borrow...
scaledDebtBalance: 900000000000000000000
isUnderLiquidation: false
After initiate liquidation...
scaledDebtBalance: 900000000000000000000
isUnderLiquidation: true
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.56ms (1.14ms CPU time)
Ran 1 test suite in 11.12ms (7.56ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

  • Liquidation Process Blocked:
    Due to the incorrect dust threshold check, users who have repaid enough to still hold a significant amount of debt (≥1e6) are unable to close liquidation, resulting in a denial of service for closing liquidation.

  • User Frustration and System Instability:
    Affected users remain trapped in liquidation despite having a non-negligible debt balance, which undermines the intended risk mitigation process and may lead to broader operational issues.

Tools Used

  • Manual Review

  • Foundry (forge)

Recommendations

To fix this vulnerability, the debt validation check in closeLiquidation should be inverted. Replace the current check:

if (userDebt > DUST_THRESHOLD) revert DebtNotZero();

with the following:

- if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
+ if (userDebt < DUST_THRESHOLD) revert DebtNotZero();

This change ensures that liquidation can only be closed if the user's remaining debt is significant (i.e., at least 1e6). If the remaining debt is below this threshold, the function will revert, preventing closure of liquidation when there is insufficient debt to justify the process.

After applying this modification, rerun the test suite to confirm that the liquidation closure behaves as intended.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

theirrationalone Submitter
7 months ago
inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!