- Published on
【Free MEV系列】| AAVE V2 清算
- Authors
- Name
- thinkingchaindotapp
AAVE-V2清算
简介
我们演示了这个清算,寻找最大的清算获利情况,我们获得了43.830663994245593622个ETH
如下图,我们用Excel来具体的计算本次清算前后的情况:
对于我来说,找到一个合适的清算策略并没有技巧,我只是不断地尝试然后进行测试,找到我能够找到的最高获利。我寻找的方向有:
- 闪电贷借款多少USDT来清算呢?借款越多,手续费越高,但是清算得到的奖励越多
- 选择怎么样的path去swap能够获利最多呢?借款的USDT数量又会影响滑点
如下图:
我选择了:
- 闪电贷借款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。