Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Macho Tartan Finch - Liquidation is not possible in certain conditions with eMode #190

Open
sherlock-admin2 opened this issue Jan 22, 2025 · 0 comments

Comments

@sherlock-admin2
Copy link

Macho Tartan Finch

Medium

Liquidation is not possible in certain conditions with eMode

Summary

Liquidation is not possible in certain conditions with eMode. If the HF value is lower than specific point in eMode usage. eMode allows user to borrow more amount of funds with high LTV and high liquidation threshold.

For instance currently, 0.95 LT and 1.01 Liquidation Bonus is used in Aave live eMode configuration.

Root Cause

Mathematically, there is a strong relation between $LT * Bonus$ and HF.

We can say that if following condition meets the bad debt is already created:

All the numbers are in base currency

$$Debt * Bonus > Collateral$$

We can also define HF as:

$$(LT * Collateral) / Debt = HF$$

Then:

$$Bonus > Collateral / Debt$$

$$HF / LT = Collateral / Debt$$

$$Bonus > HF / LT$$

$$Bonus * LT > HF$$

In conclusion if $Bonus * LT > HF$ the bad debt is already created

Currently, live Aave configuration is using 0.95 LT and 1.01 Bonus. $0.95 * 1.01 = 0.9595$. We also know that positions can be liquidated up to 50% if the HF is between 0.95 and 1 level. Therefore, we can say that bad debt will occur if HF is between 0.95 and 0.9595 and it can only liquidate up to 50% of the debt.

This is not a big problem because after liquidation the debt will decrease significantly and after reaching below to 2000e8 ( MIN_BASE ) point, we can fully liquidate the position.

But this is not possible in certain cases because bad debt may lower the collateral amount below MIN_LEFTOVER point and it may revert.

Reference1
Reference2

    // to prevent accumulation of dust on the protocol, it is enforced that you either
    // 1. liquidate all debt
    // 2. liquidate all collateral
    // 3. leave more than MIN_LEFTOVER_BASE of collateral & debt
    if (
      vars.actualDebtToLiquidate < vars.userReserveDebt &&
      vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount <
      vars.userCollateralBalance
    ) {
      bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) *
        vars.debtAssetPrice) /
        vars.debtAssetUnit >=
        MIN_LEFTOVER_BASE;

      bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance -
        vars.actualCollateralToLiquidate -
        vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) /
        vars.collateralAssetUnit >=
        MIN_LEFTOVER_BASE;
    }

Internal Pre-conditions

$$HF < LT * Bonus$$

External Pre-conditions

No response

Attack Path

  1. At mentioned point liquidation won't be possible
  2. Liquidators should wait until HF goes down below 0.95 level or HF should be increased by the borrower
  3. Of course second one is not an option here, waiting for HF will cause loss of funds because it will create more deficit

Impact

Liquidation is not possible in mentioned situation, liquidation call will revert due to bad debt. It creates DoS attack vector in time-sensitive function. It may cause loss of funds in the end.

PoC

Following PoC fuzz test will find an edgecase scenario which cause revert due to error 103 ( MUST_NOT_LEAVE_DUST ):

Add this test to Pool.Liquidations.CloseFactor.t.sol file

Add following lines to LiquidationLogic.sol for logging ( also import forge-std/console2 ):

    if (
      vars.actualDebtToLiquidate < vars.userReserveDebt &&
      vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount <
      vars.userCollateralBalance
    ) {
      bool isDebtMoreThanLeftoverThreshold = ((vars.userReserveDebt - vars.actualDebtToLiquidate) *
        vars.debtAssetPrice) /
        vars.debtAssetUnit >=
        MIN_LEFTOVER_BASE;

      bool isCollateralMoreThanLeftoverThreshold = ((vars.userCollateralBalance -
        vars.actualCollateralToLiquidate -
        vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) /
        vars.collateralAssetUnit >=
        MIN_LEFTOVER_BASE;

+    console2.log("isCollateralMoreThanLeftoverThreshold :",isCollateralMoreThanLeftoverThreshold);
+    console2.log("isDebtMoreThanLeftoverThreshold :",isDebtMoreThanLeftoverThreshold);
+    console2.log("Leaving collat: %8e",((vars.userCollateralBalance -
+      vars.actualCollateralToLiquidate -
+      vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) /
+       vars.collateralAssetUnit);
+    console2.log("Leaving debt: %8e", ((vars.userReserveDebt - vars.actualDebtToLiquidate) *
+       vars.debtAssetPrice) /
+       vars.debtAssetUnit);
      require(
        isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold,
        Errors.MUST_NOT_LEAVE_DUST
      ); 
    }

Also change the configuration of USDX in reserve for eMode configuration ( in AaveV3TestListing.sol file) :

    // change the configuration of USDX
    listingsCustom[0] = IEngine.ListingWithCustomImpl(
      IEngine.Listing({
        asset: USDX_ADDRESS,
        assetSymbol: 'USDX',
        priceFeed: USDX_MOCK_PRICE_FEED,
        rateStrategyParams: rateParams,
        enabledToBorrow: EngineFlags.ENABLED,
        borrowableInIsolation: EngineFlags.DISABLED,
        withSiloedBorrowing: EngineFlags.DISABLED,
        flashloanable: EngineFlags.ENABLED,
        ltv: 82_50,
        liqThreshold: 95_00,
        liqBonus: 1_00,
        reserveFactor: 10_00,
        supplyCap: 0,
        borrowCap: 0,
        debtCeiling: 0,
        liqProtocolFee: 10_00
      }),
      IEngine.TokenImplementations({
        aToken: ATOKEN_IMPLEMENTATION,
        vToken: VARIABLE_DEBT_TOKEN_IMPLEMENTATION
      })
    );

Command : forge test --match-test test_fuzz_wrong_state -vv

  /// forge-config: default.fuzz.show-logs = true
  /// forge-config: default.fuzz.runs = 10000
  function test_fuzz_wrong_state(uint256 debtAmount, uint256 hf, uint256 collatA) public {
    uint256 hf = bound(hf, 9545e14, 9594e14);
    uint256 debtAmount = bound(debtAmount, 1950e18, 2050e18);
    uint256 collatA = bound(collatA, 1950e6, 2050e6);
    uint256 conf = contracts.poolProxy.getConfiguration(tokenList.usdx).data;
    uint256 lt = (conf & 0x00000000000000000000000000000000000000000000000000000000FFFF0000) >> 16;

    // For changing the indexes from 1 to floating number
    vm.startPrank(whale);
    IERC20Detailed(tokenList.usdx).approve(address(contracts.poolProxy), type(uint256).max);
    contracts.poolProxy.borrow(tokenList.usdx, 1e12, 2, 0, whale);
    vm.stopPrank();

    vm.warp(block.timestamp + 365 days);

    _supplyToPool(tokenList.usdx, bob, collatA);

    vm.mockCall(
      address(contracts.aaveOracle),
      abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, tokenList.weth),
      abi.encode(0)
    );
    vm.prank(bob);
    contracts.poolProxy.borrow(tokenList.weth, debtAmount, 2, 0, bob);
    vm.clearMockedCalls();

    vm.warp(block.timestamp + 700 days);

    // collatPrice calculation for simulating 2000e8 debt worth with desired HF
    // divide by 1e14 is used for stabilizing the decimal between HF and LT (LT 1e4 base, HF 1e18 base)
    uint256 collatPrice = 1e6 * hf * 2000e8 / ( lt * collatA * 1e14);

    // debtPrice calculation for simulating 2000e8 debt worth with desired HF
    // multiply by 1e14 is used for stabilizing the decimal between HF and LT (LT 1e4 base, HF 1e18 base)
    uint256 debtPrice = collatA * collatPrice * lt * 1e18 * 1e14 / (hf * debtAmount * 1e6); 
                                                                                                

    vm.mockCall(
      address(contracts.aaveOracle),
      abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, tokenList.weth),
      abi.encode(debtPrice)
    );
    vm.mockCall(
      address(contracts.aaveOracle),
      abi.encodeWithSelector(IPriceOracleGetter.getAssetPrice.selector, tokenList.usdx),
      abi.encode(collatPrice)
    );

    (uint256 realCollatAmount, uint256 realDebtAmount, , , , uint256 hfReal) = contracts.poolProxy.getUserAccountData(bob);

    // Double Check
    console2.log("Collateral Amount with Unit: %6e", collatA);
    console2.log("Debt Amount with Unit: %18e", debtAmount);
    console2.log("Collat Price: %8e", collatPrice);
    console2.log("Debt Price: %8e", debtPrice);
    console2.log("Real HF: %18e", hfReal);
    console2.log("Desired HF ( Can be higher than the real HF due to interest ): %18e", hf);
    console2.log("RealDebtAmount: %8e", realDebtAmount);
    console2.log("RealCollatAmount: %8e", realCollatAmount);

    vm.startPrank(liquidator);
    IERC20Detailed(tokenList.weth).approve(address(contracts.poolProxy), type(uint256).max);
    contracts.poolProxy.liquidationCall(tokenList.usdx, tokenList.weth, bob, type(uint256).max, false);

Output:

  Bound result 954500000000000000
  Bound result 2000000000001202986602
  Bound result 1972149009
  Collateral Amount with Unit: 1972.149009
  Debt Amount with Unit: 2000.000000001202986602
  Collat Price: 1.01892589
  Debt Price: 0.99999999
  Real HF: 0.954174780384122213
  Desired HF ( Can be higher than the real HF due to interest ): 0.9545
  RealDebtAmount: 2000.6819836
  RealCollatAmount: 2009.47399192
  isCollateralMoreThanLeftoverThreshold : false
  isDebtMoreThanLeftoverThreshold : true
  Leaving collat: 999.12959052
  Leaving debt: 1000.3409918

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 143.98ms (22.02ms CPU time)

Ran 1 test suite in 152.03ms (143.98ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in tests/protocol/pool/Pool.Liquidations.CloseFactor.t.sol:PoolLiquidationCloseFactorTests
[FAIL: revert: 103; counterexample: calldata=0xdc21b5f60000000000000000000000000000000000000000000000000000000047b41e5600000000000000000000000000000000000000000000000000000000000000001bc7e0b9edc701f342bbfda95635f906055b4a14bfda2a98a43a4246f0547627 args=[1202986582 [1.202e9], 0, 12565600481451785587908446371263567056398965197496007045610761804412894869031 [1.256e76]]] test_fuzz_wrong_state(uint256,uint256,uint256) (runs: 3, μ: 1010869, ~: 1010870)

Mitigation

Reduce LT * Bonus below to 0.95 level or implement another logic which increase the close factor to 100% in guaranteed bad debt scenarios.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant