sherlock-audit / 2023-05-ironbank-judging

2 stars 2 forks source link

dy - Liquidation of full debt fails at sufficiently low collateral price #388

Closed sherlock-admin closed 1 year ago

sherlock-admin commented 1 year ago

dy

medium

Liquidation of full debt fails at sufficiently low collateral price

Summary

When liquidating an undercollateralized borrower, the protocol will calculate the amount of IBTokens to seize for the liquidator based on the price of the borrowed asset. If this price has dipped significantly since the loan was taken out, the calculated amount may be greater than the borrower's IBToken balance, which will cause the liquidation to revert.

Vulnerability Detail

The function IronBank._getLiquidationSeizeAmount() calculates the amount of IBToken that the liquidator will seize from the borrower using this formula:

\frac{repayAmount \times (liquidationBonus \times borrowMarketPrice) }{collateralMarketPrice \times exchangeRate}

The amount of tokens to seize will increase as collateralMarketPrice decreases, with all else held constant. For sufficiently low values of collateralMarketPrice, the amount to seize could be greater than the borrower's total balance.

In IronBank.liquidate(), this value is passed directly to IronBank._transferIBToken().

// Seize the collateral.
uint256 ibTokenAmount = _getLiquidationSeizeAmount(marketBorrow, marketCollateral, mCollateral, repayAmount);
_transferIBToken(marketCollateral, mCollateral, borrower, liquidator, ibTokenAmount);

If ibTokenAmount is greater than the the borrower's collateral balance, the require() on line 873 will fail, reverting the liquidation.

The view function IronBank.calculateLiquidationOpportunity() also uses _getLiquidationSeizeAmount(), so under these circumstances, it will return a seizable amount greater than the borrower's balance.

Impact

Severely undercollateralized borrowers cannot be liquidated for their total debt in a single call to liquidate(). Partial liquidations for small enough amounts may still be possible. Additionally, IronBank.calculateLiquidationOpportunity() will return an incorrect value under these circumstances.

Code Snippet

A Foundry test based on test/TestLiquidate.sol which demonstrates this issue is provided below:

    function testLiquidationFailure() public {
        // initial conditions
        (uint256 collateralValue, uint256 debtValue) = ib.getAccountLiquidity(user1);
        assertEq(collateralValue, 120_000e18);
        assertEq(debtValue, 100_000e18);
        assertFalse(ib.isUserLiquidatable(user1));

        // reduce price from 1500 to 200
        int256 newMarket1Price = 200e8;
        setPriceToRegistry(registry, admin, address(market1), Denominations.USD, newMarket1Price);

        // calculateLiquidationOpportunity indicates 550 available
        assertEq(ib.calculateLiquidationOpportunity(address(market2), address(market1), 500e18), 550e18);

        // User2 liquidates user1.
        uint256 repayAmount = type(uint256).max;

        vm.startPrank(user2);
        market2.approve(address(ib), repayAmount);

        ib.liquidate(user2, user1, address(market2), address(market1), repayAmount);
        vm.stopPrank();
    }

To run the POC, add the function to TestLiquidate and run this forge command:

forge test --match-test testLiquidationFailure

The test should fail with the message "transfer amount exceeds balance".

Tool used

Forge Fuzz Testing

Recommendation

A check could be added to _getLiquidationSeizeAmount() that returns the borrower's balance if it is lower than the seizable amount calculated.