Core Contracts

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

Users Can Be Liquidated In A Healthy State

Summary

Neither closeLiquidationor finalizeLiquidationperform a proper check on the users' health factor allowing users with health positions to be liquidated

Vulnerability Details

Both closeLiquidation and finalizeLiquidation functions fail to check the user's health factor before proceeding with liquidations.

The closeLiquidation function currently requires users to fully repay their debt by checking:

if (userDebt > DUST_THRESHOLD) revert DebtNotZero();

While the finalizeLiquidation function performs no health checks before liquidating the users and transferring the user's NFTs to the stability pool. This means users who have repaid enough of their debt to be in a healthy position can still be liquidated.

PoC

Add these tests to the Liquidation tests in LendingPool.test.js

// VULNERABILITY TESTS
it("should allow liquidation of healthy position if debt not fully repaid during grace period", async function () {
// Drop house price to trigger liquidation
await raacHousePrices.setHousePrice(1, ethers.parseEther("60"));
await lendingPool.connect(user2).initiateLiquidation(user1.address);
// User partially repays debt to get back to healthy position
const partialRepayAmount = ethers.parseEther("50");
await crvusd.connect(user1).approve(rToken.target, partialRepayAmount);
await lendingPool.connect(user1).repay(partialRepayAmount);
// Verify position is now healthy
const healthFactor = await lendingPool.calculateHealthFactor(user1.address);
expect(healthFactor).to.be.gt(await lendingPool.healthFactorLiquidationThreshold());
// Advance time past grace period
await ethers.provider.send("evm\_increaseTime", \[73 \* 60 \* 60]);
await ethers.provider.send("evm\_mine");
// Set owner as stability pool and fund it
await lendingPool.connect(owner).setStabilityPool(owner.address);
await crvusd.connect(owner).mint(owner.address, ethers.parseEther("100"));
await crvusd.connect(owner).approve(lendingPool.target, ethers.parseEther("100"));
// Liquidation still succeeds despite healthy position
await expect(lendingPool.connect(owner).finalizeLiquidation(user1.address))
.to.emit(lendingPool, "LiquidationFinalized");
// Verify NFT was transferred to stability pool
expect(await raacNFT.ownerOf(1)).to.equal(owner.address);
});
it("should allow liquidation of healthy position if closeLiquidation not called", async function () {
// Drop house price to trigger liquidation
await raacHousePrices.setHousePrice(1, ethers.parseEther("60"));
await lendingPool.connect(user2).initiateLiquidation(user1.address);
// User fully repays debt but doesn't call closeLiquidation
const userDebt = await lendingPool.getUserDebt(user1.address);
await crvusd.connect(user1).approve(rToken.target, userDebt);
await lendingPool.connect(user1).repay(userDebt);
// Verify position is now healthy
const healthFactor = await lendingPool.calculateHealthFactor(user1.address);
expect(healthFactor).to.be.gt(await lendingPool.healthFactorLiquidationThreshold());
// Advance time past grace period
await ethers.provider.send("evm\_increaseTime", \[73 \* 60 \* 60]);
await ethers.provider.send("evm\_mine");
// Set owner as stability pool and fund it
await lendingPool.connect(owner).setStabilityPool(owner.address);
await crvusd.connect(owner).mint(owner.address, ethers.parseEther("100"));
await crvusd.connect(owner).approve(lendingPool.target, ethers.parseEther("100"));
// Liquidation still succeeds despite healthy position
await expect(lendingPool.connect(owner).finalizeLiquidation(user1.address))
.to.emit(lendingPool, "LiquidationFinalized");
// Verify NFT was transferred to stability pool
expect(await raacNFT.ownerOf(1)).to.equal(owner.address);
});

Impact

Users can be liquidated even if in a healthy position.

Tools Used

  • manual review

Recommendations

Add a check for the users health factor in closeLiquidationand finializeLiquidationbefore proceeding with the liquidation:

uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor < healthFactorLiquidationThreshold) ... precced with liquidation
Updates

Lead Judging Commences

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

LendingPool::finalizeLiquidation() never checks if debt is still unhealthy

Support

FAQs

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

Give us feedback!