Created
December 24, 2021 16:42
-
-
Save matthelmer/1d4a2fb15874301ee1c2c19092abbce9 to your computer and use it in GitHub Desktop.
contracts/LiquidationOperator.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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