15,000 USDC
View results
Submission Details
Severity: high
Valid

System can have un-liquidatable loans even while solvent.

Summary

It is a known issue that if the system is insolvent, loans can be broken. However, even if the system is solvent, positions can be illiquidatable under certain conditions as described below.

Vulnerability Details

The liquidate function requires the health factor of a position to improve, which is enforced with the following lines:

if (endingUserHealthFactor <= startingUserHealthFactor) {
revert DSCEngine__HealthFactorNotImproved();
}

The objective is to show that a solvent position can be created which always violates this rule, and is thus illiquidatable.

Lets assume the total collateral in USD amounts is , and the debt is . Health factor is defined as , due to the 200% collateralization.

After a liquidation event of an amount say delta, the debt decreases to: , and the collateral decreases to:

The factor 1.1 is the liquidation incentive, and the liquidator gets to take out 1.1 times the amount of debt they pay off, both denominated in usd.

New health factor is thus . We want to show that this is always less than the old health factor, which is , for any choice of liquidation amount delta.

It can be trivially shown that for any , liquidation of any amount will always result in a lower health factor, and is left as an exercise for the reader.

Solvency is defined as , since this means the system has enough collateral to cover the debt. Thus if the position is , it is solvent as well as illiquidatable, which is a high severity isssue.

This can be demonstrated with a simple POC:

Assume collateral = 1000 USD. Debt = 950 USD. Thus the system is solvent, but the position is supposed to be liquidatable. But this is not the case, since liquidating by any amount will always reduce its health factor. If the entire debt is paid off, the health factor should hit 0. But the issue with this is that the system tries to pay the liquidator 1.1*950USD=1045USD, which is more than the collateral available, and thus leads to a revert due to underflow. This functionality can be seen in the github link above.

The following foundry test creates a position as described, as is shown to be illiquidatable. This is a fuzz test showing that any value passed is unable to liquidate. The minimum bound is set to 1 ether since for very low values passed, the rounding error lets the transaction go through, but it is impractical to expect liquidations of hundreds of wei.

function testAttack(uint256 liqamount) public {
liqamount = bound(liqamount, 1 ether, 950 ether);
// Create position
vm.startPrank(user);
amountCollateral = 1 ether;
amountToMint = 950 ether;
MockV3Aggregator(ethUsdPriceFeed).updateAnswer(2000e8); // 1 ETH = $2000
ERC20Mock(weth).approve(address(dsce), amountCollateral);
dsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);
vm.stopPrank();
// Price changes to illiquidatable condition
MockV3Aggregator(ethUsdPriceFeed).updateAnswer(1000e8); // 1 ETH = $1000
// Proof of solvency
(uint256 debt, uint256 collat) = dsce.getAccountInformation(user);
assert(collat > debt);
// Fail liquidate
vm.startPrank(liquidator);
deal(address(dsc), liquidator, 1000 ether);
dsc.approve(address(dsce), 1000 ether);
vm.expectRevert();
dsce.liquidate(weth, user, liqamount);
vm.stopPrank();
}

This position can be created during flash crashes, where the token can devalue large amounts in short periods of time, and liquidations cannot go through due to network congestion. Lending protocols make the fundamental assumption that any position which is solvent should be profitably liquidatable when health factor is below 1. This contract breaks this assumption as is thus a high severity issue.

Impact

Positions can be illiquidatable while being solvent.

Tools Used

Foundry

Recommendations

When calculating liquidation incentives, cap it to the amount of collateral available. This will prevent the liquidator from being paid more than the collateral available, and thus prevent the liquidation from reverting, allowing complete liquidations at less than 10% liquidation incentives.

if(totalCollateralToRedeem > s_collateralDeposited[user][token]){
totalCollateralToRedeem = s_collateralDeposited[user][token];
}

Support

FAQs

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