Published on
【Free MEV系列】| AAVE V2 清算
Authors
  • avatar
    Name
    thinkingchaindotapp
    Twitter

AAVE-V2清算

简介

我们演示了这个清算,寻找最大的清算获利情况,我们获得了43.830663994245593622个ETH

如下图,我们用Excel来具体的计算本次清算前后的情况:

image-20241107113529407

对于我来说,找到一个合适的清算策略并没有技巧,我只是不断地尝试然后进行测试,找到我能够找到的最高获利。我寻找的方向有:

  • 闪电贷借款多少USDT来清算呢?借款越多,手续费越高,但是清算得到的奖励越多
  • 选择怎么样的path去swap能够获利最多呢?借款的USDT数量又会影响滑点

如下图:

image-20241107113539246

我选择了:

  • 闪电贷借款1744500000000数量的USDT进行清算(别问我如何得到的,我只是保持path不变,不断的修改这个数字尝试,我是一个新手,但我相信一定有某种模式可以得到更好的策略)
  • 使用path=[WBTC=>WETH]的路径还款闪电贷(虽然借的是USDT,但是uniswap_V2只要维持K值即可通过检查);
  • 使用path=[WBTC=>WETH]的路径来得到我想要的ETH。

最后,我发现了:

  • 并不是使得healthFactor越接近1,获利就一定越高
  • 无法找到最佳的策略,我们只能不断的寻找更合适的策略
  • 本处并没有考虑gas,如果你使用更多的策略来进行清算、swap等,消耗的gas将会更多。我们需要考虑他,因为我们的获利需要能够覆盖gas,在网络拥堵的时候,他更加值得考虑

代码

//SPDX-License-Identifier: Unlicense
pragma solidity =0.8.19;

import "forge-std/Test.sol";
import "../interface.sol";

// @tx hash: 0xac7df37a43fab1b130318bbb761861b8357650db2e2c6493b73d6da3d9581077
// @profit : 43.830663994245593622 ETH

contract LiquidationOperator is IUniswapV2Callee, Test {

    // user The address of the borrower getting liquidated: loan of USDT collateralized with WBTC
    address user_to_be_liquidated = 0x59CE4a2AC5bC3f5F225439B2993b86B42f6d3e9F; 

    // The tokens to interact with
    IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
    IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
    IWETH WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
     
    ILendingPool lending_pool = ILendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); // aave v2
    IUniswapV2Router02 uniswap_router = IUniswapV2Router02(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); // swap
    IUniswapV2Pair weth_usdt_uniswap = IUniswapV2Pair(0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852); // flashloan

    function setUp() public {
        // hash in eth: 0xac7df37a43fab1b130318bbb761861b8357650db2e2c6493b73d6da3d9581077
        vm.createSelectFork("mainnet", 12489620 - 1);
    }

    function test_liquidation() public {
        (uint256 totalCollateralETH, uint256 totalDebtETH, uint256 availableBorrowsETH, uint256 currentLiquidationThreshold, uint256 ltv, uint256 healthFactor) = lending_pool.getUserAccountData(user_to_be_liquidated);
        console.log();
        emit log_named_decimal_uint(
            "Before totalCollateralETH", totalCollateralETH, 18
        );
        emit log_named_decimal_uint(
            "Before totalDebtETH", totalDebtETH, 18
        );
        emit log_named_decimal_uint(
            "Before availableBorrowsETH", availableBorrowsETH, 18
        );
        emit log_named_decimal_uint(
            "Before currentLiquidationThreshold", currentLiquidationThreshold, 4
        );
        emit log_named_decimal_uint(
            "Before ltv", ltv, 4
        );
        emit log_named_decimal_uint(
            "Before healthFactor", healthFactor, 18
        );

        console.log();

        uint256 beforeLiquidation = address(this).balance;
        this.operate();
        uint256 afterLiquidation = address(this).balance;
        emit log_named_decimal_uint(
            "   Profit ETH", afterLiquidation - beforeLiquidation, 18
        );

        console.log();
        (totalCollateralETH, totalDebtETH, availableBorrowsETH, currentLiquidationThreshold, ltv, healthFactor) = lending_pool.getUserAccountData(user_to_be_liquidated);
        emit log_named_decimal_uint(
            "After totalCollateralETH", totalCollateralETH, 18
        );
        emit log_named_decimal_uint(
            "After totalDebtETH", totalDebtETH, 18
        );
        emit log_named_decimal_uint(
            "After availableBorrowsETH", availableBorrowsETH, 18
        );
        emit log_named_decimal_uint(
            "After currentLiquidationThreshold", currentLiquidationThreshold, 4
        );
        emit log_named_decimal_uint(
            "After ltv", ltv, 4
        );
        emit log_named_decimal_uint(
            "After healthFactor", healthFactor, 18
        );
    }

    // required by the testing script, entry for your liquidation call
    function operate() external {

        (, , , , , uint256 healthFactor) = lending_pool.getUserAccountData(user_to_be_liquidated); 
        require(healthFactor < 1e18, "health factor should below 1 before liquidation");

        // 1744500000000 is not the best, I just change it and try again and again
        uint256 flashloan_for_usdt = 2744500000000; 
        emit log_named_decimal_uint(
            "   Flashloan for USDT(WETH-USDT)", flashloan_for_usdt, 6
        );

        // flashloan and do the liquidation in the uniswapV2Call()
        weth_usdt_uniswap.swap(0, flashloan_for_usdt, address(this), "Go to the uniswapV2Call()");

        uint256 my_wbtc_balance = WBTC.balanceOf(address(this));
        WBTC.approve(address(uniswap_router), type(uint256).max); 
        address[] memory pair = new address[](2); 
        pair[0] = address(WBTC);
        pair[1] = address(WETH);

        console.log("           (swap[uni_v2]: WBTC => WETH to myself)");

        // swap: wbtc => weth
        uniswap_router.swapExactTokensForTokens(my_wbtc_balance, 0, pair, address(this), type(uint64).max);

        my_wbtc_balance = WBTC.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My WBTC balance after swap to withdraw", my_wbtc_balance, 8
        );
        uint256 my_USDT_balance = USDT.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My USDT balance after swap to withdraw", my_USDT_balance, 6
        );
        uint256 my_WETH_balance = WETH.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My WETH balance after swap to withdraw", my_WETH_balance, 18
        );

        // withdraw: weth => eth
        uint256 weth_balance = WETH.balanceOf(address(this));
        WETH.withdraw(weth_balance);
    }

    function uniswapV2Call( // WETH_USDT callback
        address,
        uint256, 
        uint256 amount1, // The amount of USDT I flashloan
        bytes calldata
    ) external override {

        USDT.approve(address(lending_pool), type(uint256).max); // prepare for swap
        (uint112 reserves_weth, uint112 reserves_usdt, ) = IUniswapV2Pair(msg.sender).getReserves();

        // false => get the underlying collateral asset: WBTC, not aWBTC
        // amount1 is the amount we want to liquidate
        lending_pool.liquidationCall(address(WBTC), address(USDT), user_to_be_liquidated, amount1, false);

        console.log("           (use USDT to liquidate, how much to liquidate can get more profit is a critical issue)");

        uint256 my_wbtc_balance = WBTC.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My WBTC balance after liquidation", my_wbtc_balance, 8
        );
        uint256 my_USDT_balance = USDT.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My USDT balance after liquidation", my_USDT_balance, 6
        );
        uint256 my_WETH_balance = WETH.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My WETH balance after liquidation", my_WETH_balance, 18
        );

        WBTC.approve(address(uniswap_router), type(uint256).max);  // prepare for swap
        address[] memory path = new address[](2);
        path[0] = address(WBTC);
        path[1] = address(WETH);
        // amount1 is the amount we flashloan
        // by getAmountIn(), we can caculate the WBTC
        uint256 amount_plus_fee_in_amount0 = getAmountIn(amount1, reserves_weth, reserves_usdt);

        console.log("           (swap[uni_v2]: WBTC => WETH to pair, we can pay back WETH although we flashloan for USDT)");

        // pay back flashloan: borrow USDT, pay WETH
        uniswap_router.swapTokensForExactTokens(amount_plus_fee_in_amount0, type(uint256).max, path, msg.sender, type(uint64).max);

        my_wbtc_balance = WBTC.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My WBTC balance after paying back flashloan", my_wbtc_balance, 8
        );
        my_USDT_balance = USDT.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My USDT balance after paying back flashloan", my_USDT_balance, 6
        );
        my_WETH_balance = WETH.balanceOf(address(this));
        emit log_named_decimal_uint(
            "       My WETH balance after paying back flashloan", my_WETH_balance, 18
        );

    }

    ///////////////////////      view/pure functions     /////////////////////////////

    function getAmountIn(
        uint256 amountOut,
        uint256 reserveIn,
        uint256 reserveOut
    ) internal pure returns (uint256 amountIn) {
        require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");
        require(
            reserveIn > 0 && reserveOut > 0,
            "UniswapV2Library: INSUFFICIENT_LIQUIDITY"
        );
        uint256 numerator = reserveIn * amountOut * 1000;
        uint256 denominator = (reserveOut - amountOut) * 997;
        amountIn = (numerator / denominator) + 1;
    }

    receive() external payable {
        // to receive ETH
    }
}

优化

上面的清算并不够好,还有很大的空间。我们懂得了大概的原理之后,我们再次尝试,试着去优化它。

这一次的清算比tx1更多,获得了个85.048539834763696741ETH。

主要是采取了不同的策略:

  • 从sushi借入USDC,然后到curve将USDC换成USDT,因为清算是用USDT
  • 使用sushi的[WBTC-WETH]来swap,因为K值更大,使得滑点更低。如下计算是在swap之前,我们发现sushiswap的K值是uniswap的五倍,但是价格差距并不是特别大,使用sushiswap能得到更多的产出,因此我们获利更多
# python
uniswap_pool_wbtcweth_WBTC_price = 38844042475154482370337 / 229369102880
sushiswap_pool_wtbcweth_WBTC_price = 88902650478574486113986 / 525397180491
price_cap = uniswap_pool_wbtcweth_WBTC_price - sushiswap_pool_wtbcweth_WBTC_price
K_in_uniswap_wbtcweth=38844042475154482370337*229369102880
K_in_sushiswap_wbtcweth=88902650478574486113986*525397180491
times=K_in_sushiswap_wbtcweth/K_in_uniswap_wbtcweth # 5.2425563891353235
print("uniswap_pool_wbtcweth_WBTC_price", uniswap_pool_wbtcweth_WBTC_price)
print("sushiswap_pool_wtbcweth_WBTC_price", sushiswap_pool_wtbcweth_WBTC_price)
print("sushi K is ", times, "time to uniswap K")

# 输出
# uniswap_pool_wbtcweth_WBTC_price 169351678091.86874
# sushiswap_pool_wtbcweth_WBTC_price 169210368421.64282
# sushi K is  5.2425563891353235 time to uniswap K

下面是整个流程的输出:

Before totalCollateralETH: 10630.629178806013179408
  Before totalDebtETH: 8093.660042623032904515
  Before availableBorrowsETH: 0.000000000000000000
  Before currentLiquidationThreshold: 0.7561
  Before ltv: 0.7082
  Before healthFactor: 0.993100609584077736

     Flashloan for USDC(USDC-WETH): 2919549.181195
             (Use USDC to liquidate, how much to liquidate can get more profit is a critical issue)
         My WBTC balance: 0.00000000
         My USDT balance: 0.000000
         My USDC balance: 2919549.181195
         My WETH balance: 0.000000000000000000
             (Swap[cureve, DAI_USDC_USDT]: USDC => USDT)
         My WBTC balance: 0.00000000
         My USDT balance: 2916358.033172
         My USDC balance: 0.000000
         My WETH balance: 0.000000000000000000
             (After liquidation)
         My WBTC balance: 94.27272961
         My USDT balance: 0.000000
         My USDC balance: 0.000000
         My WETH balance: 0.000000000000000000
  test reserves_WBTC: 525397180491
  test reserves_WETH: 88902650478574486113986
             (Swap[sushi_v2]: WBTC => WETH. Pay back flashloan + fee. We borrow USDC but pay back WETH)
         My WBTC balance: 5.21848953
         My USDT balance: 0.000000
         My USDC balance: 0.000000
         My WETH balance: 0.000000000000000000
             (Swap[sushi_v2]: WBTC => WETH. We want WETH)
         My WBTC balance: 0.00000000
         My USDT balance: 0.000000
         My USDC balance: 0.000000
         My WETH balance: 85.048539834763696741

     Profit ETH: 85.048539834763696741

  After totalCollateralETH: 9062.096632528174258397
  After totalDebtETH: 6667.721364093260419515
  After availableBorrowsETH: 0.000000000000000000
  After currentLiquidationThreshold: 0.7572
  After ltv: 0.7096
  After healthFactor: 1.029110125552384781

代码

//SPDX-License-Identifier: Unlicense
pragma solidity =0.8.19;

import "forge-std/Test.sol";
import "../interface.sol";

// @tx hash: 0xac7df37a43fab1b130318bbb761861b8357650db2e2c6493b73d6da3d9581077
// @Profit : 85.048539834763696741 ETH

contract LiquidationOperator is IUniswapV2Callee, Test {

    // user The address of the borrower getting liquidated: loan of USDT collateralized with WBTC
    address public user_to_be_liquidated = 0x59CE4a2AC5bC3f5F225439B2993b86B42f6d3e9F; 

    IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IWETH WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
    IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);

    ICurvePool public curve_dai_usdc_usdt_pool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); // swap 
    ILendingPool public lending_pool = ILendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); // aave v2
    ISushiSwap_Pair public usdc_weth_pair = ISushiSwap_Pair(0x397FF1542f962076d0BFE58eA045FfA2d347ACa0); // flashloan
    ISushiSwapRouter02 public sushiswapRouter02 = ISushiSwapRouter02(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); // swap 

    function setUp() public {
        // hash in eth: 0xac7df37a43fab1b130318bbb761861b8357650db2e2c6493b73d6da3d9581077
        vm.createSelectFork("mainnet", 12489620 - 1);
    }

    function test_liquidation() public {
        (
        uint256 totalCollateralETH, 
        uint256 totalDebtETH, 
        uint256 availableBorrowsETH, 
        uint256 currentLiquidationThreshold, 
        uint256 ltv, 
        uint256 healthFactor
        ) = lending_pool.getUserAccountData(user_to_be_liquidated);
        
        console.log();
        emit log_named_decimal_uint(
            "Before totalCollateralETH", totalCollateralETH, 18
        );
        emit log_named_decimal_uint(
            "Before totalDebtETH", totalDebtETH, 18
        );
        emit log_named_decimal_uint(
            "Before availableBorrowsETH", availableBorrowsETH, 18
        );
        emit log_named_decimal_uint(
            "Before currentLiquidationThreshold", currentLiquidationThreshold, 4
        );
        emit log_named_decimal_uint(
            "Before ltv", ltv, 4
        );
        emit log_named_decimal_uint(
            "Before healthFactor", healthFactor, 18
        );

        console.log();

        uint256 beforeLiquidation = address(this).balance;
        this.operate();
        uint256 afterLiquidation = address(this).balance;
        emit log_named_decimal_uint(
            "   Profit ETH", afterLiquidation - beforeLiquidation, 18
        );

        console.log();

        (totalCollateralETH, totalDebtETH, availableBorrowsETH, currentLiquidationThreshold, ltv, healthFactor) = lending_pool.getUserAccountData(user_to_be_liquidated);
        emit log_named_decimal_uint(
            "After totalCollateralETH", totalCollateralETH, 18
        );
        emit log_named_decimal_uint(
            "After totalDebtETH", totalDebtETH, 18
        );
        emit log_named_decimal_uint(
            "After availableBorrowsETH", availableBorrowsETH, 18
        );
        emit log_named_decimal_uint(
            "After currentLiquidationThreshold", currentLiquidationThreshold, 4
        );
        emit log_named_decimal_uint(
            "After ltv", ltv, 4
        );
        emit log_named_decimal_uint(
            "After healthFactor", healthFactor, 18
        );
    }

    function operate() external {
        // flashloan for USDC: 2919549181195
        usdc_weth_pair.swap(2919549181195, 0, address(this), "Go to the call back");
    }

    function uniswapV2Call( // USDC_WETH callback
        address,
        uint256 amount0, // The amount of USDC I flashloan
        uint256, 
        bytes calldata
    ) external override {
        emit log_named_decimal_uint(
            "   Flashloan for USDC(USDC-WETH)", amount0, 6
        );

        console.log("           (Use USDC to liquidate, how much to liquidate can get more profit is a critical issue)");
        emit log_named_decimal_uint(
            "       My WBTC balance", WBTC.balanceOf(address(this)), 8
        );
        emit log_named_decimal_uint(
            "       My USDT balance", USDT.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My USDC balance", USDC.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My WETH balance", WETH.balanceOf(address(this)), 18
        );

        // Swap: USDC => USDT. We use curve, because curve is born for stablecoin swap
        USDC.approve(address(curve_dai_usdc_usdt_pool), type(uint256).max);
        curve_dai_usdc_usdt_pool.exchange(1, 2, amount0, 0);

        console.log("           (Swap[cureve, DAI_USDC_USDT]: USDC => USDT)");
        emit log_named_decimal_uint(
            "       My WBTC balance", WBTC.balanceOf(address(this)), 8
        );
        emit log_named_decimal_uint(
            "       My USDT balance", USDT.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My USDC balance", USDC.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My WETH balance", WETH.balanceOf(address(this)), 18
        );

        // execute the liquidation with USDT
        USDT.approve(address(lending_pool), type(uint256).max);
        uint256 my_usdt_balance = USDT.balanceOf(address(this));
        lending_pool.liquidationCall(address(WBTC), address(USDT), user_to_be_liquidated, my_usdt_balance, false);

        console.log("           (After liquidation)");
        emit log_named_decimal_uint(
            "       My WBTC balance", WBTC.balanceOf(address(this)), 8
        );
        emit log_named_decimal_uint(
            "       My USDT balance", USDT.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My USDC balance", USDC.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My WETH balance", WETH.balanceOf(address(this)), 18
        );

        // Pay back flashloan plus fee
        WBTC.approve(address(sushiswapRouter02), type(uint256).max);  // prepare for swap
        (uint112 reserves_usdc, uint112 reserves_weth, ) = IUniswapV2Pair(msg.sender).getReserves();

        uint256 flashloan_amount_plus_fee_in_amount1 = getAmountIn(amount0, reserves_weth, reserves_usdc); 
        address[] memory path = new address[](2);
        path[0] = address(WBTC);
        path[1] = address(WETH);
        sushiswapRouter02.swapTokensForExactTokens( 
            flashloan_amount_plus_fee_in_amount1, // flashloan amount + fee
            type(uint256).max, // all our WBTC can be swap to WETH
            path, 
            msg.sender, // receiver is pair, so this is why this step is paying back flashloan
            type(uint64).max
        );
        
        console.log("           (Swap[sushi_v2]: WBTC => WETH. Pay back flashloan + fee. We borrow USDC but pay back WETH)");
        emit log_named_decimal_uint(
            "       My WBTC balance", WBTC.balanceOf(address(this)), 8
        );
        emit log_named_decimal_uint(
            "       My USDT balance", USDT.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My USDC balance", USDC.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My WETH balance", WETH.balanceOf(address(this)), 18
        );


        // Swap: WBTC => WETH. Swap all my WBTC to WETH.
        sushiswapRouter02.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            WBTC.balanceOf(address(this)), 
            0, 
            path, 
            address(this), 
            type(uint64).max
        );

        console.log("           (Swap[sushi_v2]: WBTC => WETH. We want WETH)");
        emit log_named_decimal_uint(
            "       My WBTC balance", WBTC.balanceOf(address(this)), 8
        );
        emit log_named_decimal_uint(
            "       My USDT balance", USDT.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My USDC balance", USDC.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "       My WETH balance", WETH.balanceOf(address(this)), 18
        );

        console.log();

        // Swap: WETH => ETH
        uint256 weth_balance = WETH.balanceOf(address(this));
        WETH.withdraw(weth_balance); // That is our profit
    }

    ///////////////////////      view/pure functions     /////////////////////////////

    function getAmountIn(
        uint256 amountOut,
        uint256 reserveIn,
        uint256 reserveOut
    ) internal pure returns (uint256 amountIn) {
        require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");
        require(
            reserveIn > 0 && reserveOut > 0,
            "UniswapV2Library: INSUFFICIENT_LIQUIDITY"
        );
        uint256 numerator = reserveIn * amountOut * 1000;
        uint256 denominator = (reserveOut - amountOut) * 997;
        amountIn = (numerator / denominator) + 1;
    }

    receive() external payable {
        // to receive ETH
    }
}

总结

对于AAVE来说,最多清算50%。清算的利润影响因素较多,可以通过不同的策略来调整。就比如上面的例子,通过优化,利润直接从43ETH变成85ETH。

Tags