Core Contracts

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

Users can lose both NFTs and repaid funds due to missing liquidation status check in repay function

Summary

The LendingPool::repay() function lacks a check for liquidation status and grace period expiration, allowing users to repay debt even after their liquidation grace period has ended. This can result in users losing both their NFTs and the repaid amount.

Vulnerability Details

In the LendingPool::_repay() function does not verify if the user is under liquidation or if their grace period has expired. This creates a race condition where:

  1. A user's position becomes undercollateralized and liquidation is initiated

  2. The grace period expires

  3. The user attempts to repay their debt through repay()

  4. The repayment succeeds, taking their funds

  5. However, closeLiquidation() will revert due to expired grace period

  6. The Stability Pool can still call finalizeLiquidation() and seize the NFTs

Impact

Users who attempt to repay their debt after the grace period has expired will:

  1. Lose their repaid funds as the debt is reduced

  2. Still lose their NFTs to the Stability Pool through finalizeLiquidation()

  3. Have no way to recover either their NFTs or repaid funds

This creates a significant financial loss for users who are trying to save their positions but are unaware that their grace period has expired.

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/unit/core/pools/LendingPool/LendingPool.test.js file in the Liquidation section:

it("allows user to repay even after grace period, which makes him lose his NFT and repaid amount", async function () {
// Set Stability Pool address (using owner for this test)
await lendingPool.connect(owner).setStabilityPool(owner.address);
// Decrease house price and initiate liquidation
await raacHousePrices.setHousePrice(1, ethers.parseEther("90"));
// Initiate liquidation
await lendingPool.connect(user2).initiateLiquidation(user1.address);
// Advance time beyond grace period (72 hours)
await ethers.provider.send("evm_increaseTime", [72 * 60 * 60 + 1]);
await ethers.provider.send("evm_mine");
// User1 is able to repay debt
const userDebt = await lendingPool.getUserDebt(user1.address);
const debtTokenBalance = await debtToken.balanceOf(user1.address);
expect(debtTokenBalance).to.be.equal(userDebt);
const userBalanceBeforeRepay = await crvusd.balanceOf(user1.address);
const repayAmount = debtTokenBalance + ethers.parseEther("1");
await lendingPool.connect(user1).repay(repayAmount);
// User1's debt is repaid
const userBalanceAfterRepay = await crvusd.balanceOf(user1.address);
const userDebtAfterRepay = await lendingPool.getUserDebt(user1.address);
const debtTokenBalanceAfterRepay = await debtToken.balanceOf(user1.address);
expect(userBalanceAfterRepay).to.be.lt(userBalanceBeforeRepay - debtTokenBalance);
expect(userBalanceAfterRepay).to.be.gt(userBalanceBeforeRepay - repayAmount);
expect(userDebtAfterRepay).to.be.equal(0);
expect(debtTokenBalanceAfterRepay).to.be.equal(0);
// User1 is still under liquidation
expect(await lendingPool.isUnderLiquidation(user1.address)).to.be.true;
// User1 is not able to close liquidation
await expect(lendingPool.connect(user1).closeLiquidation())
.to.be.revertedWithCustomError(lendingPool, "GracePeriodExpired");
// Stability Pool closes liquidation
await expect(lendingPool.connect(owner).finalizeLiquidation(user1.address))
.to.emit(lendingPool, "LiquidationFinalized")
// Verify that the user is no longer under liquidation
expect(await lendingPool.isUnderLiquidation(user1.address)).to.be.false;
// Verify that the NFT has been transferred to the Stability Pool
expect(await raacNFT.ownerOf(1)).to.equal(owner.address);
});

Recommendations

Add liquidation status check in the _repay() function:

function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
+ // Add check for liquidation status
+ if (isUnderLiquidation[onBehalfOf]) {
+ if (block.timestamp > liquidationStartTime[onBehalfOf] + liquidationGracePeriod) {
+ revert GracePeriodExpired();
+ }
+ }
...
}
Updates

Lead Judging Commences

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

A borrower can LendingPool::repay to avoid liquidation but might not be able to call LendingPool::closeLiquidation successfully due to grace period check, loses both funds and collateral

Support

FAQs

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