Description & Impact
The only check inside closeLiquidation() is that the userDebt
is less than DUST_THRESHOLD of 1e6
. So if the NFT put up as collateral is a low-priced one and the debt is less than DUST_THRESHOLD
, the borrower can always call closeLiquidation()
within the grace period and escape liquidation repeatedly.
File: contracts/core/pools/LendingPool/LendingPool.sol
465:
466: * @notice Allows a user to repay their debt and close the liquidation within the grace period
467: */
468: function closeLiquidation() external nonReentrant whenNotPaused {
469: address userAddress = msg.sender;
470:
471: if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
472:
473:
474: ReserveLibrary.updateReserveState(reserve, rateData);
475:
476: if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
477: revert GracePeriodExpired();
478: }
479:
480: UserData storage user = userData[userAddress];
481:
482: uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
483:
484:@---> if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
485:
486: isUnderLiquidation[userAddress] = false;
487: liquidationStartTime[userAddress] = 0;
488:
489: emit LiquidationClosed(userAddress);
490: }
Imagine:
NFT collateral value = 1.1e6
Borrowed = 1e6. This (1 / 1.1 = 90.9%
) is more than the liquidation threshold of 80%.
Liquidator calls initiateLiquidation()
.
Borrower simply calls closeLiquidation()
within the grace period.
Mitigation
Add a check that any remaining debt has to be healthy:
File: contracts/core/pools/LendingPool/LendingPool.sol
465: /**
466: * @notice Allows a user to repay their debt and close the liquidation within the grace period
467: */
468: function closeLiquidation() external nonReentrant whenNotPaused {
469: address userAddress = msg.sender;
470:
471: if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
472:
473: // update state
474: ReserveLibrary.updateReserveState(reserve, rateData);
475:
476: if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
477: revert GracePeriodExpired();
478: }
479:
480: UserData storage user = userData[userAddress];
481:
482: uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
483:
484: if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
- 485:
+ 485: require (calculateHealthFactor(userAddress) >= healthFactorLiquidationThreshold, "remaining debt is unhealthy");
486: isUnderLiquidation[userAddress] = false;
487: liquidationStartTime[userAddress] = 0;
488:
489: emit LiquidationClosed(userAddress);
490: }