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();
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();
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 () {
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
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);
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 -------------------');
let hf = await lendingPool.calculateHealthFactor(user1.address);
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)
console.log('healthFactor now greater than healthFactorLiquidationThreshold : ' + (Number(hf) > Number(hflt)).toString());
await expect(lendingPool.connect(user1).closeLiquidation()).to.be.revertedWithCustomError(lendingPool, "DebtNotZero");
});
});