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

Compound V3 清算

原理

它提供了非常方便的接口供我们清算。我们主要需要用到这三个函数(当然生产环境需要更多的数据):

  • 判断是否可以清算
function isLiquidatable(address account) public view returns (bool)
  • 设置清算人和被清算的用户
function absorb(address absorber, address[] calldata accounts)
  • 清算
function buyCollateral(address asset, uint minAmount, uint baseAmount, address recipient) external

我们模仿的这个清算,也是后跑,跟在Forward()后面,因为在Forward()之后,状态机更新,用户达到可清算的条件:

清算的输出日志:

  isLiquidatable: true
     usdc: 0.000000
     weth: 8.036936676600142848
     link: 0.000000000000000000
  swap: weth => usdc
     usdc: 20286.729768
     weth: 0.000000000000000000
     link: 0.000000000000000000
  start liquidate
     absorb()
     buyCollateral(): usdc => link
  end liquidate
     usdc: 0.000000
     weth: 0.000000000000000000
     link: 2062.583568565004243502
  swap: link => weth
     usdc: 0.000000
     weth: 8.933360618507175614
     link: 0.000000000000000000
  isLiquidatable: false
  weth profit: 0.896423941907032766

使用了两个swap和清算,最终获利0.896ETH(不包含gas费用)。而在实际的交易中,给了很多贿赂费给著名的Titan Builder。

所以,如果想要加入清算的MEV行列,应该考虑以下几个方面:

  • 极致的智能合约代码,节省gas费用
  • 贿赂Builder,和Forward()交易打包在一起上链(更准确的说是放在Forward()后面)
  • 低延迟的本地节点,监控协议的用户的抵押情况和健康度
  • 高效的swap路由方式,降低滑点

另外,这里提供一个快速找到清算交易的方式,以供练习:使用Etherscan的API,根据清算函数中特有的事件进行过滤。我们以Compound V3为例,buyCollateral()中有一个日志:

cast keccak "BuyCollateral(address,address,uint256,uint256)"
# 0xf891b2a411b0e66a5f0a6ff1368670fefa287a13f541eb633a386a1a9cc7046b

然后使用Etherscan(这里直接粘贴到网页,你也可以使用curl):

https://api.etherscan.io/api?module=logs&action=getLogs&fromBlock=21005082&toBlock=21315082&topic0=0xf891b2a411b0e66a5f0a6ff1368670fefa287a13f541eb633a386a1a9cc7046b&page=1&offset=1000&apikey=????

Ok,我们得到了一大堆清算的实例可供练习😄:

代码

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

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

// @tx hash: 0x11d6b57f220427c34628c3bafaaed106d8b46f5ba379824c606bbfafecd93255
// Debt Asset: LINK
// Liquidation Asset: USDC

contract LiquidationOperator is Test {

    IERC20 public usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    ICompoundV3cToken public compound_usdc_v3 = ICompoundV3cToken(0xc3d688B66703497DAA19211EEdff47f25384cdc3);
    address user = 0xF2f0B05676d1dE3920401Bb9639Ae260fdffC09f;
    IUniswapV3Pool public uniswapV3Pool_UsdcWeth = IUniswapV3Pool(0xE0554a476A092703abdB3Ef35c80e0D76d32939F);
    IUniswapV3Pool public uniswapV3Pool_LinkWeth = IUniswapV3Pool(0xa6Cc3C2531FdaA6Ae1A3CA84c2855806728693e8);
    IWETH weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    IERC20 link = IERC20(0x514910771AF9Ca656af840dff83E8264EcF986CA);
    uint256 count = 0;

    uint256 constant public wethBeginBalance = 8036936676600142848;

    function setUp() public {
        vm.createSelectFork("mainnet", 21218344);
        // check: https://etherscan.io/txs?block=21064866&p=4
        // back run: 0xb301c753d0fcd45e7e72dd665d213d726fc0b4ac36705fb222f28bfab18cf957
        vm.rollFork(bytes32(0x11d6b57f220427c34628c3bafaaed106d8b46f5ba379824c606bbfafecd93255));
        deal(address(weth), address(this), wethBeginBalance); // prepare some money
    }

    function test_liquidation() public {

        usdc.approve(address(compound_usdc_v3), type(uint256).max);

        bool isLiquidatable = compound_usdc_v3.isLiquidatable(user);
        console.log("isLiquidatable:", isLiquidatable);

        emit log_named_decimal_uint(
            "   usdc", usdc.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "   weth", weth.balanceOf(address(this)), 18
        );
        emit log_named_decimal_uint(
            "   link", link.balanceOf(address(this)), 18
        );

        // weth => usdc
        uniswapV3Pool_UsdcWeth.swap(address(this), false, int256(wethBeginBalance), 1461446703485210103287273052203988822378723970341, "");
        console.log("swap: weth => usdc");
        emit log_named_decimal_uint(
            "   usdc", usdc.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "   weth", weth.balanceOf(address(this)), 18
        );
        emit log_named_decimal_uint(
            "   link", link.balanceOf(address(this)), 18
        );
        console.log("start liquidate");

        address[] memory users = new address[](1);
        users[0] = user;
        console.log("   absorb(): set absorber and users");
        compound_usdc_v3.absorb(address(this), users);
        console.log("   buyCollateral(): usdc => link");
        compound_usdc_v3.buyCollateral(address(link), 0, 20286729768, address(this));

        console.log("end liquidate");
        emit log_named_decimal_uint(
            "   usdc", usdc.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "   weth", weth.balanceOf(address(this)), 18
        );
        emit log_named_decimal_uint(
            "   link", link.balanceOf(address(this)), 18
        );

        // link => weth
        uniswapV3Pool_LinkWeth.swap(address(this), true, int256(link.balanceOf(address(this))), 4295128740, "");
        console.log("swap: link => weth");
        emit log_named_decimal_uint(
            "   usdc", usdc.balanceOf(address(this)), 6
        );
        emit log_named_decimal_uint(
            "   weth", weth.balanceOf(address(this)), 18
        );
        emit log_named_decimal_uint(
            "   link", link.balanceOf(address(this)), 18
        );

        isLiquidatable = compound_usdc_v3.isLiquidatable(user);
        console.log("isLiquidatable:", isLiquidatable);
        
        emit log_named_decimal_uint(
            "weth profit", weth.balanceOf(address(this)) - wethBeginBalance, 18
        );
    }

    function uniswapV3SwapCallback(int256, int256, bytes calldata) public {
        if(count == 0) {
            count++;
            weth.transfer(msg.sender, wethBeginBalance);
        } else {
            link.transfer(msg.sender, link.balanceOf(address(this)));
        }
    }
}

Tags