Skip to content

Latest commit

 

History

History
319 lines (238 loc) · 12.3 KB

File metadata and controls

319 lines (238 loc) · 12.3 KB

Lone Wintergreen Rattlesnake

High

Liquidators can force bad debt through partial liquidations while full liquidations lack incentives

Summary

In the liquidation mechanism, liquidators can intentionally perform partial liquidations to force bad debt accumulation and the protocol treats this as clean Liquidation Fee, while liquidators receive no clean liquidation fee benefits for full liquidations, creating misaligned incentives.

Root Cause

In the liquidation logic, when processing full liquidations (100% shares), the protocol intends to give full liquidators clean liquidation fee while this is not factored into the repayment amount calculation, causing liquidators to pay more without receiving the intended fee benefit or rather force liquidators to perform partial liquidation that will force the protocol to have bad debt, this bad debt occur not because there is insufficient collateral coverage.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

  1. Liquidator identifies a liquidatable position
  2. Instead of full liquidation, liquidator performs partial liquidation taking only profitable portions
  3. Remaining position becomes bad debt as it's too small to be profitable to liquidate

Impact

The protocol faces multiple negative impacts:

  1. Accumulation of bad debt by encouraging partial liquidations
  2. Liquidators are disincentivized from performing full liquidations
  3. Protocol solvency is threatened by growing bad debt positions
  4. Higher costs for liquidators performing full liquidations

PoC

Click to view the full test code
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

// forge
import {Test, console} from "forge-std/Test.sol";

// fraxlend
import {FraxlendPairDeployer, ConstructorParams} from "../src/contracts/FraxlendPairDeployer.sol";
import {FraxlendWhitelist} from "../src/contracts/FraxlendWhitelist.sol";
import {FraxlendPairRegistry} from "../src/contracts/FraxlendPairRegistry.sol";
import {FraxlendPair} from "../src/contracts/FraxlendPair.sol";
import {VariableInterestRate} from "../src/contracts/VariableInterestRate.sol";
import {IERC4626Extended} from "../src/contracts/interfaces/IERC4626Extended.sol";
import {ChainlinkSinglePriceOracle} from "../src/contracts/ChainlinkSinglePriceOracle.sol";
import "../src/contracts/mocks/LendingAssetVault.sol";

// mocks
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MockERC20} from "../src/contracts/mocks/MockERC20.sol";

contract Fraxlend is Test {
    /*///////////////////////////////////////////////////////////////
                            GLOBAL VARIABLES
    ///////////////////////////////////////////////////////////////*/

    // external actors
    address internal user0 = vm.addr(uint256(keccak256("User0")));
    address internal user1 = vm.addr(uint256(keccak256("User1")));
    address internal user2 = vm.addr(uint256(keccak256("User2")));

    address[] internal users = [user0, user1, user2];
    uint256[] internal _fraxPercentages = [10000, 2500, 7500, 5000];

    // fraxlend protocol actors
    address internal comptroller = vm.addr(uint256(keccak256("comptroller")));
    address internal circuitBreaker = vm.addr(uint256(keccak256("circuitBreaker")));
    address internal timelock = vm.addr(uint256(keccak256("comptroller")));

    uint16 internal fee = 100;
    uint256 internal PRECISION = 10 ** 27;

    /*///////////////////////////////////////////////////////////////
                            TEST CONTRACTS
    ///////////////////////////////////////////////////////////////*/

    // fraxlend
    FraxlendPairDeployer internal _fraxDeployer;
    FraxlendWhitelist internal _fraxWhitelist;
    FraxlendPairRegistry internal _fraxRegistry;
    VariableInterestRate internal _variableInterestRate;

    FraxlendPair internal _fraxLPToken1Peas;

    MockERC20 internal _mockDai;
    MockERC20 internal _collateral;
    MockERC20 internal _tokenB;
    MockERC20 internal _tokenC;
    ChainlinkSinglePriceOracle internal _clOracle; 
    LendingAssetVault internal _lendingAssetVault;

    event Message(string message);

    /*///////////////////////////////////////////////////////////////
                            SETUP FUNCTIONS
    ///////////////////////////////////////////////////////////////*/

    function setUp() public {
        // deploy mock tokens
        _mockDai = new MockERC20("MockDai", "MDAI");
        _collateral = new MockERC20("Collateral", "COL");
        _tokenB = new MockERC20("TokenB", "TKB");
        _tokenC = new MockERC20("TokenC", "TKC");

        _mockDai.mint(address(this), 1000000 ether);
        _collateral.mint(address(this), 1000000 ether);
        _tokenB.mint(address(this), 1000000e6);
        _tokenC.mint(address(this), 1000000 ether);

        _deployVariableInterestRate();
        _deployFraxWhitelist();
        _deployFraxPairRegistry();
        _deployFraxPairDeployer();
        _deployFraxPairs();
        _setupActors();

        _deployLendingVault();


        vm.startPrank(address(2));
        _fraxLPToken1Peas.setExternalAssetVault(IERC4626Extended(address(_lendingAssetVault)));
        vm.stopPrank();
        
    }

    function _deployVariableInterestRate() internal {
        // These values taken from existing Fraxlend Variable Rate Contract
        _variableInterestRate = new VariableInterestRate(
            "[0.5 [email protected] 5-10k] 2 days (.75-.85)",
            87500,
            200000000000000000,
            75000,
            85000,
            158247046,
            1582470460,
            3164940920000,
            172800
        );
    }

    function _deployFraxWhitelist() internal {
        _fraxWhitelist = new FraxlendWhitelist();
    }

    function _deployFraxPairRegistry() internal {
        address[] memory _initialDeployers = new address[](0);
        _fraxRegistry = new FraxlendPairRegistry(address(this), _initialDeployers);
    }

    function _deployFraxPairDeployer() internal {
        ConstructorParams memory _params =
            ConstructorParams(address(this), address(1), address(2), address(_fraxWhitelist), address(_fraxRegistry));
        _fraxDeployer = new FraxlendPairDeployer(_params);

        _fraxDeployer.setCreationCode(type(FraxlendPair).creationCode);

        address[] memory _whitelistDeployer = new address[](1);
        _whitelistDeployer[0] = address(this);

        _fraxWhitelist.setFraxlendDeployerWhitelist(_whitelistDeployer, true);

        address[] memory _registryDeployer = new address[](1);
        _registryDeployer[0] = address(_fraxDeployer);

        _fraxRegistry.setDeployers(_registryDeployer, true);
        emit Message("1");
    }

    function _deployFraxPairs() internal {
        // moving time to help out the twap
        vm.warp(block.timestamp + 1 days);

        _clOracle = new ChainlinkSinglePriceOracle(address(0));

        emit Message("1aa");

        _fraxLPToken1Peas = FraxlendPair(
            _fraxDeployer.deploy(
                abi.encode(
                    address(_mockDai), // asset
                    address(_collateral), // collateral
                    address(_clOracle), //oracle
                    5000, // 5000, // maxOracleDeviation
                    address(_variableInterestRate), //rateContract
                    1000, //fullUtilizationRate
                    75000, // maxLtv
                    10000, // uint256 _cleanLiquidationFee
                    // 9000, // uint256 _dirtyLiquidationFee
                    0 //uint256 _protocolLiquidationFee
                )
            )
        );

        emit Message("1b");

        // deposit some asset
        IERC20(address(_mockDai)).approve(address(_fraxLPToken1Peas), type(uint256).max);
        _fraxLPToken1Peas.deposit(100_000 ether, address(this));
    }

    function _deployLendingVault() internal {
        _lendingAssetVault = new LendingAssetVault("LDA", "FLDA", address(_mockDai));

        _lendingAssetVault.setVaultWhitelist(address(_fraxLPToken1Peas), true);
        address[] memory _vaults = new address[](1);
        _vaults[0] = address(_fraxLPToken1Peas);
        uint256[] memory _maxAllocations = new uint256[](1);
        _maxAllocations[0] = 100_000 ether;
        _lendingAssetVault.setVaultMaxAllocation(_vaults, _maxAllocations);


        vm.startPrank(users[0]);
        IERC20(address(_mockDai)).approve(address(_lendingAssetVault), type(uint256).max);
        _lendingAssetVault.deposit(1_000_000 ether, users[0]);

        vm.startPrank(users[1]);
        IERC20(address(_mockDai)).approve(address(_lendingAssetVault), type(uint256).max);
        _lendingAssetVault.deposit(1_000_000 ether, users[0]);
    }

    /*////////////////////////////////////////////////////////////////
                                    HELPERS
    ////////////////////////////////////////////////////////////////*/

    function _setupActors() internal {
        for (uint256 i; i < users.length; i++) {
            vm.deal(users[i], 1000000 ether);
            vm.startPrank(users[i]);
            // _weth.deposit{value: 1000000 ether}();

            _collateral.mint(users[i], 100_000 ether);
            _tokenB.mint(users[i], 1000000e6);
            _tokenC.mint(users[i], 1000000 ether);
            _mockDai.mint(users[i], 1000000 ether);
            IERC20(address(_collateral)).approve(address(_fraxLPToken1Peas), type(uint256).max);
        }
    }
}
function testFullLiquidationWithoutCleanLiquidationFeeRemoval() public {
        _clOracle.setMinAndMaxPrices(198e18, 200e18);
        vm.startPrank(users[1]);
        _fraxLPToken1Peas.borrowAsset(370 ether, 100_000 ether, users[1]);
        assertEq(_collateral.balanceOf(address(_fraxLPToken1Peas)), 100_000 ether);
        assertEq(_collateral.balanceOf(users[1]), 0);

        vm.warp(block.timestamp + 1);
        uint256 _amountLiquidatorToRepay = 370000000058774236930;

        _clOracle.setMinAndMaxPrices(260e18, 262e18);

        // Try to preview withdraw more than available
        uint256 sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
        assertEq(sharesToLiquidate, 370 ether);

        assertEq(_collateral.balanceOf(users[2]), 100_000 ether);
        assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether);
        vm.startPrank(users[2]);
        IERC20(address(_mockDai)).approve(address(_fraxLPToken1Peas), type(uint256).max);

        _fraxLPToken1Peas.liquidate(uint128(sharesToLiquidate), block.timestamp + 10, users[1]);
        assertEq(_collateral.balanceOf(users[2]), 200_000 ether);

        assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether - _amountLiquidatorToRepay);
        sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
        assertEq(sharesToLiquidate, 0);
    }

    function testPartialLiquidationWithCleanLiquidationFeeRemoval() public {
        _clOracle.setMinAndMaxPrices(198e18, 200e18);
        vm.startPrank(users[1]);
        _fraxLPToken1Peas.borrowAsset(370 ether, 100_000 ether, users[1]);
        assertEq(_collateral.balanceOf(address(_fraxLPToken1Peas)), 100_000 ether);
        assertEq(_collateral.balanceOf(users[1]), 0);

        vm.warp(block.timestamp + 1);
        uint256 _amountLiquidatorToRepay = 350000000055597251150;

        _clOracle.setMinAndMaxPrices(260e18, 262e18);

        // Try to preview withdraw more than available
        uint256 sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
        assertEq(sharesToLiquidate, 370 ether);

        assertEq(_collateral.balanceOf(users[2]), 100_000 ether);
        assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether);
        vm.startPrank(users[2]);
        IERC20(address(_mockDai)).approve(address(_fraxLPToken1Peas), type(uint256).max);

        _fraxLPToken1Peas.liquidate(350 ether, block.timestamp + 10, users[1]);
        assertEq(_collateral.balanceOf(users[2]), 200_000 ether);

        assertEq(_mockDai.balanceOf(users[2]), 1_000_000 ether - _amountLiquidatorToRepay);
        sharesToLiquidate = _fraxLPToken1Peas.userBorrowShares(users[1]);
        assertEq(sharesToLiquidate, 0);
    }

Mitigation

If it is a full liquidation, apply the clean liquidation fee benefit before calculating the repayment amount