Skip to content

Instantly share code, notes, and snippets.

@matthelmer
Created December 24, 2021 16:42
Show Gist options
  • Save matthelmer/1d4a2fb15874301ee1c2c19092abbce9 to your computer and use it in GitHub Desktop.
Save matthelmer/1d4a2fb15874301ee1c2c19092abbce9 to your computer and use it in GitHub Desktop.
contracts/LiquidationOperator.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.7;
import "hardhat/console.sol";
// ----------------------INTERFACE------------------------------
// Aave
// https://docs.aave.com/developers/the-core-protocol/lendingpool/ilendingpool
interface ILendingPool {
/**
* Function to liquidate a non-healthy position collateral-wise, with Health Factor below 1
* - The caller (liquidator) covers `debtToCover` amount of debt of the user getting liquidated, and receives
* a proportionally amount of the `collateralAsset` plus a bonus to cover market risk
* @param collateralAsset The address of the underlying asset used as collateral, to receive as result of theliquidation
* @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation
* @param user The address of the borrower getting liquidated
* @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover
* @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants
* to receive the underlying collateral asset directly
**/
function liquidationCall(
address collateralAsset,
address debtAsset,
address user,
uint256 debtToCover,
bool receiveAToken
) external;
/**
* Returns the user account data across all the reserves
* @param user The address of the user
* @return totalCollateralETH the total collateral in ETH of the user
* @return totalDebtETH the total debt in ETH of the user
* @return availableBorrowsETH the borrowing power left of the user
* @return currentLiquidationThreshold the liquidation threshold of the user
* @return ltv the loan to value of the user
* @return healthFactor the current health factor of the user
**/
function getUserAccountData(address user)
external
view
returns (
uint256 totalCollateralETH,
uint256 totalDebtETH,
uint256 availableBorrowsETH,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
);
}
// UniswapV2
// https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IERC20.sol
interface IERC20 {
// Returns the account balance of another account with address _owner.
function balanceOf(address owner) external view returns (uint256);
/**
* Allows _spender to withdraw from your account multiple times, up to the _value amount.
* If this function is called again it overwrites the current allowance with _value.
* Lets msg.sender set their allowance for a spender.
**/
function approve(address spender, uint256 value) external;
/**
* Transfers _value amount of tokens to address _to, and MUST fire the Transfer event.
* The function SHOULD throw if the message caller’s account balance does not have enough tokens to spend.
* Lets msg.sender send pool tokens to an address.
**/
function transfer(address to, uint256 value) external returns (bool);
}
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/interfaces/IWETH.sol
interface IWETH is IERC20 {
// Convert the wrapped token back to Ether.
function withdraw(uint256) external;
}
interface IUniswapV2Callee {
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
}
interface IUniswapV2Factory {
// Returns the address of the pair for tokenA and tokenB, if it has been created, else address(0).
function getPair(address tokenA, address tokenB)
external
view
returns (address pair);
}
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
function swapExactTokensForETH(
uint amountIn,
uint AmountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
interface IUniswapV2Pair {
/**
* Swaps tokens. For regular swaps, data.length must be 0.
* Also see [Flash Swaps](https://docs.uniswap.org/protocol/V2/concepts/core-concepts/flash-swaps).
**/
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external;
/**
* Returns the reserves of token0 and token1 used to price trades and distribute liquidity.
* See Pricing[https://docs.uniswap.org/protocol/V2/concepts/advanced-topics/pricing].
* Also returns the block.timestamp (mod 2**32) of the last block during which an interaction occured for the pair.
**/
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves()
external
view
returns (
uint112 reserve0,
uint112 reserve1,
uint32 blockTimestampLast
);
}
contract LiquidationOperator is IUniswapV2Callee {
uint8 public constant health_factor_decimals = 18;
// for swaps
address constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
IUniswapV2Router constant router = IUniswapV2Router(ROUTER);
// for getting pairs
address constant FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
IUniswapV2Factory factory = IUniswapV2Factory(FACTORY);
// tokens we will use
address constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
IWETH weth = IWETH(WETH);
// pair for trade capital
address USDT_WETH = factory.getPair(USDT, WETH);
IUniswapV2Pair flashPair = IUniswapV2Pair(USDT_WETH);
// pair for liquidating collateral
address WBTC_WETH = factory.getPair(WBTC, WETH);
IUniswapV2Pair collateralPair = IUniswapV2Pair(WBTC_WETH);
// lending pool stuff
address constant USER = 0x59CE4a2AC5bC3f5F225439B2993b86B42f6d3e9F;
address constant AAVE = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9;
ILendingPool lendingPool = ILendingPool(AAVE);
// returns the maximum output amount of the other asset
function getAmountOut(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut
) internal pure returns (uint256 amountOut) {
require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
require(
reserveIn > 0 && reserveOut > 0,
"UniswapV2Library: INSUFFICIENT_LIQUIDITY"
);
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
// returns a required input amount of the other asset
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;
}
constructor() {
}
// to withdraw your WETH
receive() external payable {}
function operate() external {
// security checks and initializing variables
console.log('Initial WETH balance: %d', weth.balanceOf(address(this)));
address token0 = flashPair.token0();
( // USDT-WETH reserves
uint112 reserve0,
uint112 reserve1,
) = flashPair.getReserves();
// check aave user account data
(
uint256 totalCollateral, // value of WBTC collateral (in ETH)
uint256 totalDebt, // value of USDT principal (in ETH)
,
,
,
uint256 healthFactor
) = lendingPool.getUserAccountData(USER);
require(healthFactor < (1 * 10 ** health_factor_decimals),
"Can't liquidate a healthy loan!");
// call flash swap to liquidate the target user
// borrow USDT, liquidate USER and get the WBTC, then swap WBTC to repay uniswap
// In: WETH, Out: USDT
(
uint reserveIn,
uint reserveOut
) = USDT == token0 ? (reserve1, reserve0) : (reserve0, reserve1);
// amount of Tether we will put toward loan liquidation
uint debtToCoverUSDT = 2916378221684;
// set amounts for flash swap
(
uint amount0Out,
uint amount1Out
) = USDT == token0 ? (debtToCoverUSDT, uint(0)) : (uint(0), debtToCoverUSDT);
// make this a flash swap
bytes memory data = abi.encode(USDT, debtToCoverUSDT);
// flash swap to borrow debtToCoverUSDT amount of Tether via WETH-USDT
flashPair.swap(amount0Out, amount1Out, address(this), data);
// 3. Convert the profit into ETH and send back to sender
uint weth_balance = weth.balanceOf(address(this));
console.log('WETH balance after paying back flash: %d', weth_balance);
console.log('Unwrapping WETH and calculating PnL...');
weth.withdraw(weth_balance);
payable(msg.sender).transfer(weth_balance);
console.log('Done!');
}
function uniswapV2Call(
address, uint256, uint256 amount1, bytes calldata data
) external override {
IERC20 usdt = IERC20(USDT);
// collateral is WBTC
IERC20 wbtc = IERC20(WBTC);
// total amount of USDT received in flash swap
uint amountBorrowedUSDT = usdt.balanceOf(address(this));
console.log('Amount of USDT borrowed: %d', amountBorrowedUSDT);
// WETH-USDT pair
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
(, uint debtToCoverUSDT) = abi.decode(data, (address, uint));
// allow aave to move up to debtToCoverUSDT out
usdt.approve(address(lendingPool), debtToCoverUSDT);
lendingPool.liquidationCall(WBTC, USDT, USER, debtToCoverUSDT, false);
uint collateralReceived = wbtc.balanceOf(address(this));
console.log('WBTC collateral from Aave liquidation: %d', collateralReceived);
// swap WBTC collateral for WETH
uint deadline = block.timestamp + 100;
address[] memory path_collateral;
path_collateral = new address[](2);
path_collateral[0] = WBTC;
path_collateral[1] = WETH;
wbtc.approve(address(router), collateralReceived);
router.swapExactTokensForTokens(
collateralReceived, // amountIn
uint(1), // amountOutMin
path_collateral,
address(this),
deadline
);
console.log('WBTC collateral sold!');
console.log('New WETH balance: %', weth.balanceOf(address(this)));
// repay USDT flash loan using WETH
// get amount of WETH In needed to get debtToCoverUSDT Out
(
uint112 reserve0,
uint112 reserve1,
) = flashPair.getReserves();
(
uint reserveIn,
uint reserveOut
) = USDT == token0 ? (reserve1, reserve0) : (reserve0, reserve1);
uint amountToRepayETH = getAmountIn(debtToCoverUSDT, reserveIn, reserveOut);
// return USDT as WETH
console.log('Repaying WETH to msg.sender: %d...', amountToRepayETH);
weth.transfer(msg.sender, amountToRepayETH);
console.log('Transfer complete!');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment