- Published on
【Free MEV系列】| Compound V3 清算
- Authors
- Name
- thinkingchaindotapp
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)));
}
}
}