Core Contracts

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

Borrower cannot close Liquidation even when health factor has been restored to healthy position

Target

contracts/core/pools/LendingPool/LendingPool.sol

Vulnerability Details

When a user’s health factor is below the minimum liquidation threshold, anyone can initiate a liquidation process on the user’s collateral.

function initiateLiquidation(address userAddress) external nonReentrant whenNotPaused {
if (isUnderLiquidation[userAddress]) revert UserAlreadyUnderLiquidation();
// update state
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);
}

LendingPool.initiateLiquidation

if the user repays their debt within the grace period, they can close the liquidation.

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
emit LiquidationClosed(userAddress);
}

LendingPool.closeLiquidation

However the close liquidation logic requires that the user’s debt must be fully paid before the pending liquidation can be cancelled. it does not take into account situations where the user may want to repay part of their debt thereby restoring the their debt profile to a healthy position.

Users who are facing pending liquidation are forced to repay all debts even when partial repayments could still be an option, this can lead to bad user experience for users of the protocol.

Impact

Forcing users to repay all debts to avoid liquidation can be quite inconvenient since most other protocols allow partial repayments and debt maintenance, this can lead to bad user experience for users.

Tools Used

Manual review

Recommendations

Allow users to be able to close pending liquidation as soon as the debt profile has been restored to a healthy position.

POC

Add the test script below in test/unit/core/pools/LendingPool/LendingPool.test.js

describe("POC Test #2", function () {
it("reverts during close liquidation even when user's debt position is now healthy", async function () {
// User2 deposits into the reserve pool to provide liquidity
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
// User1 deposits NFT and borrows
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("80");
await lendingPool.connect(user1).borrow(borrowAmount);
// Users approve crvUSD for potential transactions
await crvusd.connect(user2).approve(lendingPool.target, ethers.parseEther("1000"));
await crvusd.connect(owner).approve(lendingPool.target, ethers.parseEther("1000"));
await raacHousePrices.setHousePrice(1, ethers.parseEther("90"));
await lendingPool.connect(user2).initiateLiquidation(user1.address);
console.log('-------------------- before repay debt -------------------');
//health Factor
let hf = await lendingPool.calculateHealthFactor(user1.address);
// Health factor liquidation threshold
let hflt = await lendingPool.healthFactorLiquidationThreshold();
console.log('user health factor ' + hf);
console.log('health factor liquidation threshold ' + hflt)
console.log('---------------after user repays some debt ---------');
await crvusd.connect(user1).approve(lendingPool.target, ethers.parseEther("10"));
await lendingPool.connect(user1).repay(ethers.parseEther("10"));
hf = await lendingPool.calculateHealthFactor(user1.address);
hflt = await lendingPool.healthFactorLiquidationThreshold();
console.log('user health factor ' + hf);
console.log('health factor liquidation threshold ' + hflt)
//Debt position now healthy
console.log('healthFactor now greater than healthFactorLiquidationThreshold : ' + (Number(hf) > Number(hflt)).toString());
// User1 tries to close the liquidation but it reverts even when the debt position is now healthy!
await expect(lendingPool.connect(user1).closeLiquidation()).to.be.revertedWithCustomError(lendingPool, "DebtNotZero");
});
});
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!