├── tests ├── __init__.py └── test_liquidator.py ├── scripts ├── __init__.py └── liquidation.py ├── .gitattributes ├── package.json ├── .gitignore ├── interfaces ├── joecore │ ├── IJoeCallee.sol │ ├── IWAVAX.sol │ ├── IJoeFactory.sol │ ├── IERC20.sol │ ├── IJoeERC20.sol │ ├── IJoeRouter02.sol │ ├── IJoePair.sol │ └── IJoeRouter01.sol ├── ERC3156FlashBorrowerInterface.sol ├── ERC3156FlashLenderInterface.sol └── JoeLending.sol ├── pyproject.toml ├── Makefile ├── hardhat.config.js ├── README.md └── contracts └── Liquidator.sol /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "hardhat": "^2.6.8" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | .history 4 | .hypothesis/ 5 | build/ 6 | reports/ 7 | -------------------------------------------------------------------------------- /interfaces/joecore/IJoeCallee.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IJoeCallee { 6 | function joeCall( 7 | address sender, 8 | uint256 amount0, 9 | uint256 amount1, 10 | bytes calldata data 11 | ) external; 12 | } 13 | -------------------------------------------------------------------------------- /interfaces/joecore/IWAVAX.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IWAVAX { 6 | function deposit() external payable; 7 | 8 | function transfer(address to, uint256 value) external returns (bool); 9 | 10 | function withdraw(uint256) external; 11 | } 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "traderjoe-bounty" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Your Name "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8, <3.10" 9 | eth-brownie = "^1.17.1" 10 | httpx = "^0.21.1" 11 | 12 | [tool.poetry.dev-dependencies] 13 | bpython = "^0.22.1" 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | compile: 2 | poetry run brownie compile 3 | 4 | test: 5 | poetry run brownie test --network hardhat 6 | 7 | test-hardhat: clean 8 | poetry run brownie test --network hardhat 9 | 10 | test-hardhat-debug: clean 11 | poetry run brownie test --network hardhat -vv -s --interactive 12 | 13 | console: 14 | poetry run brownie console --network hardhat 15 | 16 | clean: 17 | rm -rf build/* 18 | 19 | bot: 20 | poetry run brownie run scripts/liquidation.py --network hardhat 21 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | 2 | // autogenerated by brownie 3 | // do not modify the existing settings 4 | module.exports = { 5 | defaultNetwork: "hardhat", 6 | networks: { 7 | hardhat: { 8 | gasPrice: 225000000000, 9 | initialBaseFeePerGas: 0, 10 | forking: { 11 | url: 'https://api.avax.network/ext/bc/C/rpc', 12 | blockNumber: 7300000 13 | }, 14 | // brownie expects calls and transactions to throw on revert 15 | throwOnTransactionFailures: true, 16 | throwOnCallFailures: true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /interfaces/ERC3156FlashBorrowerInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ERC3156FlashBorrowerInterface { 5 | /** 6 | * @dev Receive a flash loan. 7 | * @param initiator The initiator of the loan. 8 | * @param token The loan currency. 9 | * @param amount The amount of tokens lent. 10 | * @param fee The additional amount of tokens to repay. 11 | * @param data Arbitrary data structure, intended to contain user-defined parameters. 12 | * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" 13 | */ 14 | function onFlashLoan( 15 | address initiator, 16 | address token, 17 | uint256 amount, 18 | uint256 fee, 19 | bytes calldata data 20 | ) external returns (bytes32); 21 | } 22 | -------------------------------------------------------------------------------- /interfaces/joecore/IJoeFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IJoeFactory { 6 | event PairCreated(address indexed token0, address indexed token1, address pair, uint256); 7 | 8 | function feeTo() external view returns (address); 9 | 10 | function feeToSetter() external view returns (address); 11 | 12 | function migrator() external view returns (address); 13 | 14 | function getPair(address tokenA, address tokenB) external view returns (address pair); 15 | 16 | function allPairs(uint256) external view returns (address pair); 17 | 18 | function allPairsLength() external view returns (uint256); 19 | 20 | function createPair(address tokenA, address tokenB) external returns (address pair); 21 | 22 | function setFeeTo(address) external; 23 | 24 | function setFeeToSetter(address) external; 25 | 26 | function setMigrator(address) external; 27 | } 28 | -------------------------------------------------------------------------------- /interfaces/joecore/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IERC20 { 6 | event Approval(address indexed owner, address indexed spender, uint256 value); 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | 9 | function name() external view returns (string memory); 10 | 11 | function symbol() external view returns (string memory); 12 | 13 | function decimals() external view returns (uint8); 14 | 15 | function totalSupply() external view returns (uint256); 16 | 17 | function balanceOf(address owner) external view returns (uint256); 18 | 19 | function allowance(address owner, address spender) external view returns (uint256); 20 | 21 | function approve(address spender, uint256 value) external returns (bool); 22 | 23 | function transfer(address to, uint256 value) external returns (bool); 24 | 25 | function transferFrom( 26 | address from, 27 | address to, 28 | uint256 value 29 | ) external returns (bool); 30 | } 31 | -------------------------------------------------------------------------------- /interfaces/ERC3156FlashLenderInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "./ERC3156FlashBorrowerInterface.sol"; 4 | 5 | interface ERC3156FlashLenderInterface { 6 | /** 7 | * @dev The amount of currency available to be lent. 8 | * @param token The loan currency. 9 | * @return The amount of `token` that can be borrowed. 10 | */ 11 | function maxFlashLoan(address token) external view returns (uint256); 12 | 13 | /** 14 | * @dev The fee to be charged for a given loan. 15 | * @param token The loan currency. 16 | * @param amount The amount of tokens lent. 17 | * @return The amount of `token` to be charged for the loan, on top of the returned principal. 18 | */ 19 | function flashFee(address token, uint256 amount) external view returns (uint256); 20 | 21 | /** 22 | * @dev Initiate a flash loan. 23 | * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. 24 | * @param token The loan currency. 25 | * @param amount The amount of tokens lent. 26 | * @param data Arbitrary data structure, intended to contain user-defined parameters. 27 | */ 28 | function flashLoan( 29 | ERC3156FlashBorrowerInterface receiver, 30 | address token, 31 | uint256 amount, 32 | bytes calldata data 33 | ) external returns (bool); 34 | } 35 | -------------------------------------------------------------------------------- /interfaces/joecore/IJoeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IJoeERC20 { 6 | event Approval(address indexed owner, address indexed spender, uint256 value); 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | 9 | function name() external pure returns (string memory); 10 | 11 | function symbol() external pure returns (string memory); 12 | 13 | function decimals() external pure returns (uint8); 14 | 15 | function totalSupply() external view returns (uint256); 16 | 17 | function balanceOf(address owner) external view returns (uint256); 18 | 19 | function allowance(address owner, address spender) external view returns (uint256); 20 | 21 | function approve(address spender, uint256 value) external returns (bool); 22 | 23 | function transfer(address to, uint256 value) external returns (bool); 24 | 25 | function transferFrom( 26 | address from, 27 | address to, 28 | uint256 value 29 | ) external returns (bool); 30 | 31 | function DOMAIN_SEPARATOR() external view returns (bytes32); 32 | 33 | function PERMIT_TYPEHASH() external pure returns (bytes32); 34 | 35 | function nonces(address owner) external view returns (uint256); 36 | 37 | function permit( 38 | address owner, 39 | address spender, 40 | uint256 value, 41 | uint256 deadline, 42 | uint8 v, 43 | bytes32 r, 44 | bytes32 s 45 | ) external; 46 | } 47 | -------------------------------------------------------------------------------- /interfaces/joecore/IJoeRouter02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.6.2; 4 | 5 | import "./IJoeRouter01.sol"; 6 | 7 | interface IJoeRouter02 is IJoeRouter01 { 8 | function removeLiquidityAVAXSupportingFeeOnTransferTokens( 9 | address token, 10 | uint256 liquidity, 11 | uint256 amountTokenMin, 12 | uint256 amountAVAXMin, 13 | address to, 14 | uint256 deadline 15 | ) external returns (uint256 amountAVAX); 16 | 17 | function removeLiquidityAVAXWithPermitSupportingFeeOnTransferTokens( 18 | address token, 19 | uint256 liquidity, 20 | uint256 amountTokenMin, 21 | uint256 amountAVAXMin, 22 | address to, 23 | uint256 deadline, 24 | bool approveMax, 25 | uint8 v, 26 | bytes32 r, 27 | bytes32 s 28 | ) external returns (uint256 amountAVAX); 29 | 30 | function swapExactTokensForTokensSupportingFeeOnTransferTokens( 31 | uint256 amountIn, 32 | uint256 amountOutMin, 33 | address[] calldata path, 34 | address to, 35 | uint256 deadline 36 | ) external; 37 | 38 | function swapExactAVAXForTokensSupportingFeeOnTransferTokens( 39 | uint256 amountOutMin, 40 | address[] calldata path, 41 | address to, 42 | uint256 deadline 43 | ) external payable; 44 | 45 | function swapExactTokensForAVAXSupportingFeeOnTransferTokens( 46 | uint256 amountIn, 47 | uint256 amountOutMin, 48 | address[] calldata path, 49 | address to, 50 | uint256 deadline 51 | ) external; 52 | } 53 | -------------------------------------------------------------------------------- /interfaces/joecore/IJoePair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IJoePair { 6 | event Approval(address indexed owner, address indexed spender, uint256 value); 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | 9 | function name() external pure returns (string memory); 10 | 11 | function symbol() external pure returns (string memory); 12 | 13 | function decimals() external pure returns (uint8); 14 | 15 | function totalSupply() external view returns (uint256); 16 | 17 | function balanceOf(address owner) external view returns (uint256); 18 | 19 | function allowance(address owner, address spender) external view returns (uint256); 20 | 21 | function approve(address spender, uint256 value) external returns (bool); 22 | 23 | function transfer(address to, uint256 value) external returns (bool); 24 | 25 | function transferFrom( 26 | address from, 27 | address to, 28 | uint256 value 29 | ) external returns (bool); 30 | 31 | function DOMAIN_SEPARATOR() external view returns (bytes32); 32 | 33 | function PERMIT_TYPEHASH() external pure returns (bytes32); 34 | 35 | function nonces(address owner) external view returns (uint256); 36 | 37 | function permit( 38 | address owner, 39 | address spender, 40 | uint256 value, 41 | uint256 deadline, 42 | uint8 v, 43 | bytes32 r, 44 | bytes32 s 45 | ) external; 46 | 47 | event Mint(address indexed sender, uint256 amount0, uint256 amount1); 48 | event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); 49 | event Swap( 50 | address indexed sender, 51 | uint256 amount0In, 52 | uint256 amount1In, 53 | uint256 amount0Out, 54 | uint256 amount1Out, 55 | address indexed to 56 | ); 57 | event Sync(uint112 reserve0, uint112 reserve1); 58 | 59 | function MINIMUM_LIQUIDITY() external pure returns (uint256); 60 | 61 | function factory() external view returns (address); 62 | 63 | function token0() external view returns (address); 64 | 65 | function token1() external view returns (address); 66 | 67 | function getReserves() 68 | external 69 | view 70 | returns ( 71 | uint112 reserve0, 72 | uint112 reserve1, 73 | uint32 blockTimestampLast 74 | ); 75 | 76 | function price0CumulativeLast() external view returns (uint256); 77 | 78 | function price1CumulativeLast() external view returns (uint256); 79 | 80 | function kLast() external view returns (uint256); 81 | 82 | function mint(address to) external returns (uint256 liquidity); 83 | 84 | function burn(address to) external returns (uint256 amount0, uint256 amount1); 85 | 86 | function swap( 87 | uint256 amount0Out, 88 | uint256 amount1Out, 89 | address to, 90 | bytes calldata data 91 | ) external; 92 | 93 | function skim(address to) external; 94 | 95 | function sync() external; 96 | 97 | function initialize(address, address) external; 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Last-resort liquidation 2 | 3 | This is a sample liquidation bot for [traderjoe lending](https://traderjoexyz.com/#/lending) for the [liquidation bot bounty](https://docs.google.com/document/d/1k8GusDAk-dLO8heNG-d4YJkmx8Z8vVMsIfS1R6QeMUE/edit). This was my first _proper_ project in blockchain and is probably full of bad practices and security issues. You **should not** use this code in a production project although if you are interested in learning a bit about how compound and uniswap protocols work you may find it interesting. 4 | 5 | ## Overview 6 | 7 | The project is comprised of numerous parts. It has dependencies on the [traderjoe subgraph](https://thegraph.com/hosted-service/subgraph/traderjoe-xyz/lending?query=underwater%20accounts) and for testing uses the public avalanche node at _https://api.avax.network/ext/bc/C/rpc_. The code in this project used [brownie](https://eth-brownie.readthedocs.io) for developing the smart contracts and the python bot. 8 | 9 | The flow of the bot is fairly straightforward with just some complications in the contract due to re-entrancy protection on the trader joe contracts. These protections disallow you from calling multiple functions such as flash loan and liquidate in the same transaction. 10 | 11 | 1. A web3.py filter listens for new avalanche blocks 12 | 2. On each new block, thegraph is queried for underwater accounts 13 | 3. Each accounts tokens are analysed and the liquidation parameters are sent in a transaction to our liquidation smart contract 14 | 4. The contract flashloans a token (not the repay token) from trader joe 15 | 5. Converts that token into the loan to repay token 16 | 6. Pays the underwater loan and redeems the seized capital 17 | 7. Convert the seized capital back into the token from flashloan and repay flash loan 18 | 8. Sends any left over profit back to the contract owner 19 | 20 | ## Running the bot 21 | 22 | To run the bot you will need to install some python dependencies. This project uses [poetry](https://python-poetry.org/) to manage its python dependencies. Once you have poetry installed on your system run `poetry install` to install the required libraries. If you want to run the test-suite on a forked mainnet you also need to have hardhat installed. I installed this using [npm](https://www.npmjs.com/) `npm install hardhat`. 23 | 24 | There are a number of useful [make](https://www.gnu.org/software/make/) targets for running the parts of this bot. 25 | 26 | `make compile` compiles the smart contracts 27 | `make test-hardhat` and optionally `make test-hardhat-debug` runs the test suite. 28 | `make bot` run the actual bot 29 | 30 | The test suit contains some tests I wrote for extra learning along the way, the test `test_liquidate_borrow` is particularly useful if you want to see the end to end process of liquidating a loan without using any deployed contracts. 31 | 32 | ## Where the magic happens 33 | 34 | There are 3 main files you should look at to see how this project works. 35 | The [contract](https://github.com/jummy123/last-resort-liquidator/blob/master/contracts/Liquidator.sol) contains the solidity code with all on chain functionality. 36 | The [bot](https://github.com/jummy123/last-resort-liquidator/blob/master/scripts/liquidation.py) that contains the functionality for finding liquidation opportunities and calling our contract. 37 | the [test](https://github.com/jummy123/last-resort-liquidator/blob/master/tests/test_liquidator.py) where you can see how the code actually works. 38 | 39 | ## Things I learnt. 40 | 41 | * I had to inline a lot of variables, the solidity stack is shallow! 42 | * Using hardhat with brownie you don't have access to `TransactionReceipt.return_value` it is always `None`. 43 | * Events are usefull for debugging, view them with `TransactionReceipt.events`, this is still a PITA and I found myself longing for a [structlog](https://www.structlog.org/en/stable/) equivalent. 44 | * You really **don't** have to know JS to write/test/deploy solidity smart contracts 45 | 46 | ## TODO 47 | 48 | Things I really should have done but didn't. As I mentioned this project was a learning opportunity for me. Below is a big list of improvements that could be done to this project. There are also a lot of features in the original spec I didn't implement and **haven't** included them in this list. 49 | 50 | * Support liquidating native loans (only ERC20 supported) 51 | * Swapping profits back to AVAX 52 | * Any traderjoe swap without a direct path will fail 53 | * Accounts with not enough collateral in a single token to liquidate half an entire loan will be skipped 54 | * learn [NatSpec](https://docs.soliditylang.org/) documentation format 55 | * listen to a real node and maintain a local database of account health 56 | * Dockerfile so people don't need to install the deps 57 | * Config such be loaded from env (deployed addresses etc) 58 | -------------------------------------------------------------------------------- /interfaces/joecore/IJoeRouter01.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.6.2; 4 | 5 | interface IJoeRouter01 { 6 | function factory() external pure returns (address); 7 | 8 | function WAVAX() external pure returns (address); 9 | 10 | function addLiquidity( 11 | address tokenA, 12 | address tokenB, 13 | uint256 amountADesired, 14 | uint256 amountBDesired, 15 | uint256 amountAMin, 16 | uint256 amountBMin, 17 | address to, 18 | uint256 deadline 19 | ) 20 | external 21 | returns ( 22 | uint256 amountA, 23 | uint256 amountB, 24 | uint256 liquidity 25 | ); 26 | 27 | function addLiquidityAVAX( 28 | address token, 29 | uint256 amountTokenDesired, 30 | uint256 amountTokenMin, 31 | uint256 amountAVAXMin, 32 | address to, 33 | uint256 deadline 34 | ) 35 | external 36 | payable 37 | returns ( 38 | uint256 amountToken, 39 | uint256 amountAVAX, 40 | uint256 liquidity 41 | ); 42 | 43 | function removeLiquidity( 44 | address tokenA, 45 | address tokenB, 46 | uint256 liquidity, 47 | uint256 amountAMin, 48 | uint256 amountBMin, 49 | address to, 50 | uint256 deadline 51 | ) external returns (uint256 amountA, uint256 amountB); 52 | 53 | function removeLiquidityAVAX( 54 | address token, 55 | uint256 liquidity, 56 | uint256 amountTokenMin, 57 | uint256 amountAVAXMin, 58 | address to, 59 | uint256 deadline 60 | ) external returns (uint256 amountToken, uint256 amountAVAX); 61 | 62 | function removeLiquidityWithPermit( 63 | address tokenA, 64 | address tokenB, 65 | uint256 liquidity, 66 | uint256 amountAMin, 67 | uint256 amountBMin, 68 | address to, 69 | uint256 deadline, 70 | bool approveMax, 71 | uint8 v, 72 | bytes32 r, 73 | bytes32 s 74 | ) external returns (uint256 amountA, uint256 amountB); 75 | 76 | function removeLiquidityAVAXWithPermit( 77 | address token, 78 | uint256 liquidity, 79 | uint256 amountTokenMin, 80 | uint256 amountAVAXMin, 81 | address to, 82 | uint256 deadline, 83 | bool approveMax, 84 | uint8 v, 85 | bytes32 r, 86 | bytes32 s 87 | ) external returns (uint256 amountToken, uint256 amountAVAX); 88 | 89 | function swapExactTokensForTokens( 90 | uint256 amountIn, 91 | uint256 amountOutMin, 92 | address[] calldata path, 93 | address to, 94 | uint256 deadline 95 | ) external returns (uint256[] memory amounts); 96 | 97 | function swapTokensForExactTokens( 98 | uint256 amountOut, 99 | uint256 amountInMax, 100 | address[] calldata path, 101 | address to, 102 | uint256 deadline 103 | ) external returns (uint256[] memory amounts); 104 | 105 | function swapExactAVAXForTokens( 106 | uint256 amountOutMin, 107 | address[] calldata path, 108 | address to, 109 | uint256 deadline 110 | ) external payable returns (uint256[] memory amounts); 111 | 112 | function swapTokensForExactAVAX( 113 | uint256 amountOut, 114 | uint256 amountInMax, 115 | address[] calldata path, 116 | address to, 117 | uint256 deadline 118 | ) external returns (uint256[] memory amounts); 119 | 120 | function swapExactTokensForAVAX( 121 | uint256 amountIn, 122 | uint256 amountOutMin, 123 | address[] calldata path, 124 | address to, 125 | uint256 deadline 126 | ) external returns (uint256[] memory amounts); 127 | 128 | function swapAVAXForExactTokens( 129 | uint256 amountOut, 130 | address[] calldata path, 131 | address to, 132 | uint256 deadline 133 | ) external payable returns (uint256[] memory amounts); 134 | 135 | function quote( 136 | uint256 amountA, 137 | uint256 reserveA, 138 | uint256 reserveB 139 | ) external pure returns (uint256 amountB); 140 | 141 | function getAmountOut( 142 | uint256 amountIn, 143 | uint256 reserveIn, 144 | uint256 reserveOut 145 | ) external pure returns (uint256 amountOut); 146 | 147 | function getAmountIn( 148 | uint256 amountOut, 149 | uint256 reserveIn, 150 | uint256 reserveOut 151 | ) external pure returns (uint256 amountIn); 152 | 153 | function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); 154 | 155 | function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); 156 | } 157 | -------------------------------------------------------------------------------- /scripts/liquidation.py: -------------------------------------------------------------------------------- 1 | """ 2 | A script for liquidating underwater flash loans using 3 | the Liquidator flash loaning contract. 4 | """ 5 | 6 | import os 7 | from collections import namedtuple 8 | import time 9 | import random 10 | import sys 11 | 12 | from brownie import web3, convert, accounts 13 | from brownie import Liquidator 14 | import httpx 15 | 16 | 17 | LIQUIDATOR_ADDRESS = "0x0" # Add your deployed liquidator address here. 18 | 19 | 20 | WAVAX = "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7" 21 | WETH = "0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab" 22 | WBTC = "0x50b7545627a5162f82a992c33b87adc75187b218" 23 | USDC = "0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664" 24 | USDT = "0xc7198437980c041c805a1edcba50c1ce5db95118" 25 | DAI = "0xd586e7f844cea2f87f50152665bcbc2c279d8d70" 26 | LINK = "0x5947bb275c521040051d82396192181b413227a3" 27 | MIM = "0x130966628846bfd36ff31a822705796e8cb8c18d" 28 | XJOE = "0x57319d41f71e81f3c65f2a47ca4e001ebafd4f33" 29 | 30 | JAVAX = "0xC22F01ddc8010Ee05574028528614634684EC29e" 31 | JWETH = "0x929f5caB61DFEc79a5431a7734a68D714C4633fa" 32 | JWBTC = "0x3fE38b7b610C0ACD10296fEf69d9b18eB7a9eB1F" 33 | JUSDC = "0xEd6AaF91a2B084bd594DBd1245be3691F9f637aC" 34 | JUSDT = "0x8b650e26404AC6837539ca96812f0123601E4448" 35 | JDAI = "0xc988c170d0E38197DC634A45bF00169C7Aa7CA19" 36 | JLINK = "0x585E7bC75089eD111b656faA7aeb1104F5b96c15" 37 | JMIM = "0xcE095A9657A02025081E0607c8D8b081c76A75ea" 38 | JXJOE = "0xC146783a59807154F92084f9243eb139D58Da696" 39 | 40 | # Provides a lookup for underlying token addresses. 41 | JOE_TO_ERC20 = { 42 | JAVAX: WAVAX, 43 | JWETH: WETH, 44 | JWBTC: WBTC, 45 | JUSDC: USDC, 46 | JUSDT: USDT, 47 | JDAI: DAI, 48 | JLINK: LINK, 49 | JMIM: MIM, 50 | JXJOE: XJOE, 51 | } 52 | 53 | 54 | TRADER_JOB_LENDING_SUBGRAPH_URL = ( 55 | "https://api.thegraph.com/subgraphs/name/traderjoe-xyz/lending") 56 | 57 | UNDERWATER_ACCOUNTS_QUERY = """\ 58 | {{ 59 | accounts(where: {{ 60 | health_gt: {health_gt}, 61 | health_lt: {health_lt}, 62 | totalBorrowValueInUSD_gt: {borrow_value_usd_gt} }} 63 | ) {{ 64 | id 65 | health 66 | totalBorrowValueInUSD 67 | totalCollateralValueInUSD 68 | tokens {{ 69 | id 70 | symbol 71 | supplyBalanceUnderlying 72 | borrowBalanceUnderlying 73 | enteredMarket 74 | }} 75 | }} 76 | }} 77 | """ 78 | 79 | MARKET_QUERY = """\ 80 | { 81 | markets { 82 | id 83 | symbol 84 | underlyingPriceUSD 85 | } 86 | } 87 | """ 88 | 89 | LiquidationParameters = namedtuple( 90 | 'LiquidationParameters', [ 91 | 'borrower', 92 | 'liquidation_contract', 93 | 'liquidation_underlying', 94 | 'collateral_contract', 95 | 'collateral_underlying', 96 | 'flashloan_contract', 97 | 'flashloan_underlying', 98 | ]) 99 | 100 | 101 | def query_underwater_accounts( 102 | health_lt=1.0, 103 | health_gt=0, 104 | borrow_value_usd_gt=0 105 | ): 106 | """ 107 | Query thegraph API to find loans with given health values and underlying 108 | borrowed collaterall. 109 | """ 110 | query = UNDERWATER_ACCOUNTS_QUERY.format( 111 | health_lt=health_lt, 112 | health_gt=health_gt, 113 | borrow_value_usd_gt=borrow_value_usd_gt, 114 | ) 115 | response = httpx.post( 116 | TRADER_JOB_LENDING_SUBGRAPH_URL, 117 | json={"query": query}, 118 | ) 119 | response.raise_for_status() 120 | return response.json()['data']['accounts'] 121 | 122 | 123 | def query_underling_price_usd(): 124 | """ 125 | Get the current USD price for all banker joe markets 126 | """ 127 | response = httpx.post( 128 | TRADER_JOB_LENDING_SUBGRAPH_URL, 129 | json={"query": MARKET_QUERY}, 130 | ) 131 | response.raise_for_status() 132 | return { 133 | oracle['symbol']: oracle['underlyingPriceUSD'] 134 | for oracle in 135 | response.json()['data']['markets'] 136 | } 137 | 138 | 139 | def liquidation_parameters(accounts): 140 | """ 141 | Iterator over a series of underwater accounts. 142 | 143 | Yields `LiquidationParameters` named tuples containing 144 | the parameters for our liquidation contract. 145 | """ 146 | 147 | # supplyBalanceUnderlying > 0 148 | # enterMarket == true (otherwise it’s not posted as collateral) 149 | # Must have enough supplyBalanceUnderlying to seize 50% of borrow value 150 | for account in accounts: 151 | 152 | # We use a naieve algorithm here. We first find the max 153 | # seizable collateral and then find an account that has 154 | # borrowed more than double of this. Our contract doesn't 155 | # do this logic for us so we either get all or nothing here. 156 | # If there is no single collateral we can seize to repay 157 | # a full amount the account is skipped (lucky borrower). 158 | max_seizable = max([ 159 | token 160 | for token in account['tokens'] 161 | if token['enteredMarket'] is True 162 | and float(token['supplyBalanceUnderlying']) > 0], 163 | key=lambda t: float(t['supplyBalanceUnderlying'])) 164 | max_repayable = max([ 165 | token 166 | for token in account['tokens'] 167 | if token['enteredMarket'] is True 168 | and ( 169 | (float(token['borrowBalanceUnderlying']) / 2) 170 | < float(max_seizable['supplyBalanceUnderlying']))], 171 | key=lambda t: float(t['borrowBalanceUnderlying'])) 172 | if not max_seizable and max_repayable: 173 | continue 174 | 175 | # Addresses aren't checksumed in graph response. 176 | repayable = convert.to_address(max_repayable['id'].split('-')[0]) 177 | seizable = convert.to_address(max_seizable['id'].split('-')[0]) 178 | 179 | # For flash loaning we just choose one token not represented here. 180 | flash_loanable = set(JOE_TO_ERC20) 181 | flash_loanable.remove(repayable) 182 | try: 183 | flash_loanable.remove(seizable) 184 | except KeyError: 185 | # collateral and borrowed same token. 186 | pass 187 | 188 | # Choose a random token for our flash loan. 189 | flash_loan = random.choice(list(flash_loanable)) 190 | 191 | yield LiquidationParameters( 192 | convert.to_address(account['id']), 193 | repayable, 194 | JOE_TO_ERC20[repayable], 195 | seizable, 196 | JOE_TO_ERC20[seizable], 197 | flash_loan, 198 | JOE_TO_ERC20[flash_loan], 199 | ) 200 | 201 | 202 | def main(): 203 | """ 204 | Our main function that. 205 | 1. Listens for new blocks. 206 | 2. Queries the graph. 207 | 3. Sends liquidation params to our flash loan contract. 208 | """ 209 | # Our pre-deployed liquidator contract. 210 | liquidator = Liquidator.at(LIQUIDATOR_ADDRESS) 211 | 212 | # Filter for new blocks. 213 | new_blocks = web3.eth.filter('latest') 214 | 215 | # Continuous loop waiting for new blocks. 216 | while True: 217 | if new_blocks.get_new_entries(): 218 | accounts = query_underwater_accounts() 219 | for liquidation_params in liquidation_parameters(accounts): 220 | try: 221 | Liquidator.liquidateLoan(*liquidation_params, {'from': accounts[0]}) 222 | except brownie.exceptions.VirtualMachineError as exc: 223 | print(f"Exception liquidation loan {exc}", file=sys.stderr) 224 | else: 225 | # Call to discord etc. 226 | print(f"Liquidated loan {liquidation_params}") 227 | 228 | 229 | if __name__ == "__main__": 230 | main() 231 | -------------------------------------------------------------------------------- /contracts/Liquidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "interfaces/joecore/IERC20.sol"; 6 | import "interfaces/joecore/IWAVAX.sol"; 7 | import "interfaces/joecore/IJoeRouter02.sol"; 8 | 9 | import "interfaces/JoeLending.sol"; 10 | import "interfaces/ERC3156FlashBorrowerInterface.sol"; 11 | import "interfaces/ERC3156FlashLenderInterface.sol"; 12 | 13 | 14 | contract Liquidator is ERC3156FlashBorrowerInterface { 15 | /* 16 | // Event emitted by our contract. 17 | */ 18 | // Event emitted after performing a trader-joe swap. 19 | event Swapped( 20 | address fromTokenAddress, 21 | address toTokenAddress, 22 | uint fromTokenAmount, 23 | uint toTokensAmount 24 | ); 25 | // Event emitted when we receive a flash loan. 26 | event Flashloaned( 27 | address tokenAddress, 28 | uint amount, 29 | uint fee 30 | ); 31 | // Event emitted when we liquidate an loan. 32 | event Liquidated( 33 | address accountAddress, 34 | address tokenAddress, 35 | uint amount 36 | ); 37 | // Event used for debugging. 38 | event Debug( 39 | string key, 40 | string stringValue, 41 | uint uintValue, 42 | address addressValue 43 | ); 44 | 45 | address public owner; // Where to send profits of liquidating 46 | 47 | // Addresses of contracts used by this contract. 48 | address public WAVAX = 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7; 49 | 50 | // Interfaces for contracts we interact with. 51 | IJoeRouter02 public joeRouter; // Interface for the trader joe router. 52 | JComptroller public joeComptroller; // Interface for comptroller. 53 | 54 | constructor(address joeRouterAddress, address joeComptrollerAddress) { 55 | owner = msg.sender; 56 | joeRouter = IJoeRouter02(joeRouterAddress); 57 | joeComptroller = JComptroller(joeComptrollerAddress); 58 | } 59 | 60 | // Swap all the `tokenFrom` this contract holds to `tokenTo`. 61 | function swapERC20(address tokenFrom, address tokenTo) internal { 62 | require(IERC20(tokenFrom).balanceOf(address(this)) > 0, "Contract has no balance of tokenFrom"); 63 | 64 | uint amountFrom = IERC20(tokenFrom).balanceOf(address(this)); 65 | 66 | IERC20(tokenFrom).approve(address(joeRouter), amountFrom); 67 | address[] memory path = new address[](2); 68 | path[0] = tokenFrom; 69 | path[1] = tokenTo; 70 | 71 | joeRouter.swapExactTokensForTokens( 72 | amountFrom, 73 | 1, // XXX: Should not have 1 Wei minimum out. 74 | path, 75 | address(this), 76 | block.timestamp + 1 minutes); 77 | require(IERC20(tokenTo).balanceOf(address(this)) > 0, "Didn't receive token"); 78 | emit Swapped( 79 | tokenFrom, 80 | tokenTo, 81 | amountFrom, 82 | IERC20(tokenTo).balanceOf(address(this))); 83 | } 84 | 85 | // Swap all native avax for tokens. 86 | function swapFromNative(address tokenTo) internal { 87 | require(address(this).balance > 0, "Contract has no native balance"); 88 | 89 | uint amountAvax = address(this).balance; 90 | address[] memory path = new address[](2); 91 | path[0] = WAVAX; 92 | path[1] = tokenTo; 93 | 94 | // XXX: Should not have 1 Wei minimum out. 95 | joeRouter.swapExactAVAXForTokens{value: amountAvax}( 96 | 1, 97 | path, 98 | address(this), 99 | block.timestamp + 1 minutes); 100 | 101 | require(IERC20(tokenTo).balanceOf(address(this)) > 0, "Didn't receive token"); 102 | emit Swapped( 103 | WAVAX, 104 | tokenTo, 105 | amountAvax, 106 | IERC20(tokenTo).balanceOf(address(this))); 107 | } 108 | 109 | function swapToNative(address tokenFrom) internal { 110 | require(IERC20(tokenFrom).balanceOf(address(this)) > 0, "Contract has no balance of tokenFrom"); 111 | 112 | uint amountFrom = IERC20(tokenFrom).balanceOf(address(this)); 113 | address[] memory path = new address[](2); 114 | path[0] = tokenFrom; 115 | path[1] = WAVAX; 116 | IERC20(tokenFrom).approve(address(joeRouter), amountFrom); 117 | joeRouter.swapExactTokensForAVAX( 118 | amountFrom, 119 | 1, 120 | path, 121 | address(this), 122 | block.timestamp + 1 minutes); 123 | 124 | require(address(this).balance > 0, "has no native balance"); 125 | emit Swapped( 126 | tokenFrom, 127 | WAVAX, 128 | IERC20(tokenFrom).balanceOf(address(this)), 129 | address(this).balance); 130 | } 131 | 132 | function liquidateLoan( 133 | address borrower, 134 | address jTokenLiquidateAddress, 135 | address jTokenLiquidateUnderlying, 136 | address jTokenCollateral, 137 | address jTokenCollateralUnderlying, 138 | address jTokenFlashLoan, 139 | address jTokenFlashLoanUnderlying 140 | ) external { 141 | // So the steps are as follows. 142 | // 1. Work out how much we need to repay. 143 | // 2. Work out how much we need to flash loan. 144 | // 3. Flash loan a token that is not the token to repay (non re-entrant). 145 | // 4. Swap the token for the loan to repay. 146 | // 5. Repay the loan. 147 | // 6. Withdraw the seized funds. 148 | // 7. Repay the flashloan 149 | // 9. Send the seized funds to the owner. 150 | 151 | // Due to the re-entrant protection on trader joe 152 | // we must flash loan a token that will not be 153 | // seized or liquidated. 154 | 155 | // 1. How much we need to repay. 156 | uint repayAmount = amountToRepay( 157 | borrower, 158 | jTokenLiquidateAddress, 159 | jTokenFlashLoanUnderlying); 160 | 161 | // 2. How much we need to flashloan. 162 | uint flashLoanAmount = getFlashLoanAmount( 163 | jTokenFlashLoanUnderlying, 164 | jTokenLiquidateUnderlying, 165 | repayAmount); 166 | 167 | // Data to pass through to the callback function. 168 | bytes memory data = abi.encode( 169 | borrower, 170 | repayAmount, 171 | jTokenLiquidateAddress, 172 | jTokenLiquidateUnderlying, 173 | jTokenCollateral, 174 | jTokenCollateralUnderlying, 175 | jTokenFlashLoanUnderlying 176 | ); 177 | // 3. Perform the flash loan. 178 | ERC3156FlashLenderInterface(jTokenFlashLoan).flashLoan( 179 | this, 180 | jTokenFlashLoan, 181 | flashLoanAmount, 182 | data); 183 | } 184 | 185 | function onFlashLoan( 186 | address initiator, 187 | address token, 188 | uint256 amount, 189 | uint256 fee, 190 | bytes calldata data 191 | ) override external returns (bytes32) { 192 | emit Flashloaned(token, amount, fee); 193 | require(joeComptroller.isMarketListed(msg.sender), "untrusted message sender"); 194 | 195 | ( 196 | address borrower, 197 | uint repayAmount, 198 | address jTokenLiquidateAddress, 199 | address jTokenLiquidateUnderlying, 200 | address jTokenCollateral, 201 | address jTokenCollateralUnderlying, 202 | address jTokenFlashLoanUnderlying 203 | ) = abi.decode(data, ( 204 | address, uint, address, address, address, address, address 205 | )); 206 | // 4. Swap the flash loan for the amount we will repay. 207 | swapERC20(jTokenFlashLoanUnderlying, jTokenLiquidateUnderlying); 208 | 209 | // 5. Liquidate the borrower. 210 | // Approve the jtoken to spend our repayment. 211 | liquidateBorrower( 212 | jTokenLiquidateUnderlying, 213 | jTokenLiquidateAddress, 214 | borrower, 215 | repayAmount, 216 | jTokenCollateral 217 | ); 218 | emit Liquidated(borrower, jTokenLiquidateAddress, repayAmount); 219 | 220 | // 6. Withdraw the seized funds. 221 | JToken(jTokenCollateral).redeem(JToken(jTokenCollateral).balanceOf(address(this))); 222 | 223 | // 7. Repay the flash loan. 224 | swapERC20(jTokenCollateralUnderlying, jTokenFlashLoanUnderlying); 225 | 226 | IERC20(token).approve(msg.sender, amount + fee); 227 | 228 | // 8. Send seized funds to owner. 229 | IERC20(jTokenFlashLoanUnderlying).transfer( 230 | owner, 231 | IERC20(jTokenFlashLoanUnderlying).balanceOf(address(this)) - (amount + fee)); 232 | 233 | // Done. 234 | return keccak256("ERC3156FlashBorrowerInterface.onFlashLoan"); 235 | } 236 | 237 | // 1. How much we need to repay. 238 | function amountToRepay( 239 | address borrower, 240 | address jTokenLiquidateAddress, 241 | address jTokenFlashLoanUnderlying 242 | ) internal returns (uint) { 243 | uint borrowBalance = JToken(jTokenLiquidateAddress).borrowBalanceCurrent(borrower); 244 | uint closeFactor = joeComptroller.closeFactorMantissa(); 245 | uint repayAmount = (borrowBalance * closeFactor) / (10 ** 18); 246 | return repayAmount; 247 | } 248 | 249 | // 2. How much we need to flashloan. 250 | function getFlashLoanAmount( 251 | address jTokenFlashLoanUnderlying, 252 | address jTokenLiquidateUnderlying, 253 | uint repayAmount 254 | ) internal returns (uint) { 255 | address[] memory path = new address[](2); 256 | path[0] = jTokenFlashLoanUnderlying; 257 | path[1] = jTokenLiquidateUnderlying; 258 | uint flashLoanAmount = joeRouter.getAmountsIn(repayAmount, path)[0]; 259 | return flashLoanAmount; 260 | } 261 | 262 | // 5. Liquidate the borrower. 263 | function liquidateBorrower( 264 | address jTokenLiquidateUnderlying, 265 | address jTokenLiquidateAddress, 266 | address borrower, 267 | uint repayAmount, 268 | address jTokenCollateral 269 | ) internal { 270 | // Approve the jtoken to spend our repayment. 271 | IERC20(jTokenLiquidateUnderlying).approve( 272 | jTokenLiquidateAddress, 273 | IERC20(jTokenLiquidateUnderlying).balanceOf(address(this))); 274 | // The actual liquidation 275 | JToken(jTokenLiquidateAddress).liquidateBorrow( 276 | borrower, 277 | repayAmount, 278 | JToken(jTokenCollateral)); 279 | } 280 | 281 | // Function to allow us to fund our contract with seed funds. 282 | // Not actually needed. 283 | function fund () public payable {} 284 | fallback() external payable {} 285 | } 286 | 287 | 288 | // A contract we just use for testing 289 | // XXX: Do not deploy this contract. 290 | contract TestLiquidator is Liquidator { 291 | constructor( 292 | address joeRouterAddress, 293 | address joeComptrollerAddress 294 | ) Liquidator( 295 | joeRouterAddress, 296 | joeComptrollerAddress 297 | ) {} 298 | 299 | function _swapERC20(address tokenFrom, address tokenTo) public { 300 | swapERC20(tokenFrom, tokenTo); 301 | } 302 | function _swapFromNative(address tokenTo) public { 303 | swapFromNative(tokenTo); 304 | } 305 | 306 | function _swapToNative(address tokenFrom) public { 307 | swapToNative(tokenFrom); 308 | } 309 | 310 | } 311 | -------------------------------------------------------------------------------- /interfaces/JoeLending.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | interface JToken { 4 | 5 | enum Error { 6 | NO_ERROR, 7 | UNAUTHORIZED, 8 | JOETROLLER_MISMATCH, 9 | INSUFFICIENT_SHORTFALL, 10 | INSUFFICIENT_LIQUIDITY, 11 | INVALID_CLOSE_FACTOR, 12 | INVALID_COLLATERAL_FACTOR, 13 | INVALID_LIQUIDATION_INCENTIVE, 14 | MARKET_NOT_ENTERED, // no longer possible 15 | MARKET_NOT_LISTED, 16 | MARKET_ALREADY_LISTED, 17 | MATH_ERROR, 18 | NONZERO_BORROW_BALANCE, 19 | PRICE_ERROR, 20 | REJECTION, 21 | SNAPSHOT_ERROR, 22 | TOO_MANY_ASSETS, 23 | TOO_MUCH_REPAY 24 | } 25 | 26 | enum FailureInfo { 27 | ACCEPT_ADMIN_PENDING_ADMIN_CHECK, 28 | ACCEPT_PENDING_IMPLEMENTATION_ADDRESS_CHECK, 29 | EXIT_MARKET_BALANCE_OWED, 30 | EXIT_MARKET_REJECTION, 31 | SET_CLOSE_FACTOR_OWNER_CHECK, 32 | SET_CLOSE_FACTOR_VALIDATION, 33 | SET_COLLATERAL_FACTOR_OWNER_CHECK, 34 | SET_COLLATERAL_FACTOR_NO_EXISTS, 35 | SET_COLLATERAL_FACTOR_VALIDATION, 36 | SET_COLLATERAL_FACTOR_WITHOUT_PRICE, 37 | SET_IMPLEMENTATION_OWNER_CHECK, 38 | SET_LIQUIDATION_INCENTIVE_OWNER_CHECK, 39 | SET_LIQUIDATION_INCENTIVE_VALIDATION, 40 | SET_MAX_ASSETS_OWNER_CHECK, 41 | SET_PENDING_ADMIN_OWNER_CHECK, 42 | SET_PENDING_IMPLEMENTATION_OWNER_CHECK, 43 | SET_PRICE_ORACLE_OWNER_CHECK, 44 | SUPPORT_MARKET_EXISTS, 45 | SUPPORT_MARKET_OWNER_CHECK, 46 | SET_PAUSE_GUARDIAN_OWNER_CHECK 47 | } 48 | 49 | 50 | /** 51 | * @notice Event emitted when interest is accrued 52 | */ 53 | event AccrueInterest(uint256 cashPrior, uint256 interestAccumulated, uint256 borrowIndex, uint256 totalBorrows); 54 | 55 | /** 56 | * @notice Event emitted when tokens are minted 57 | */ 58 | event Mint(address minter, uint256 mintAmount, uint256 mintTokens); 59 | 60 | /** 61 | * @notice Event emitted when tokens are redeemed 62 | */ 63 | event Redeem(address redeemer, uint256 redeemAmount, uint256 redeemTokens); 64 | 65 | /** 66 | * @notice Event emitted when underlying is borrowed 67 | */ 68 | event Borrow(address borrower, uint256 borrowAmount, uint256 accountBorrows, uint256 totalBorrows); 69 | 70 | /** 71 | * @notice Event emitted when a borrow is repaid 72 | */ 73 | event RepayBorrow( 74 | address payer, 75 | address borrower, 76 | uint256 repayAmount, 77 | uint256 accountBorrows, 78 | uint256 totalBorrows 79 | ); 80 | 81 | /** 82 | * @notice Event emitted when a borrow is liquidated 83 | */ 84 | event LiquidateBorrow( 85 | address liquidator, 86 | address borrower, 87 | uint256 repayAmount, 88 | address jTokenCollateral, 89 | uint256 seizeTokens 90 | ); 91 | 92 | event Failure(uint256 error, uint256 info, uint256 detail); 93 | 94 | function transfer(address dst, uint256 amount) external returns (bool); 95 | 96 | function transferFrom( 97 | address src, 98 | address dst, 99 | uint256 amount 100 | ) external returns (bool); 101 | 102 | function approve(address spender, uint256 amount) external returns (bool); 103 | 104 | function allowance(address owner, address spender) external view returns (uint256); 105 | 106 | function balanceOf(address owner) external view returns (uint256); 107 | 108 | function balanceOfUnderlying(address owner) external returns (uint256); 109 | 110 | function getAccountSnapshot(address account) 111 | external 112 | view 113 | returns ( 114 | uint256, 115 | uint256, 116 | uint256, 117 | uint256 118 | ); 119 | 120 | function borrowRatePerSecond() external view returns (uint256); 121 | 122 | function supplyRatePerSecond() external view returns (uint256); 123 | 124 | function totalBorrowsCurrent() external returns (uint256); 125 | 126 | function borrowBalanceCurrent(address account) external returns (uint256); 127 | 128 | function borrowBalanceStored(address account) external view returns (uint256); 129 | 130 | function exchangeRateCurrent() external returns (uint256); 131 | 132 | function exchangeRateStored() external view returns (uint256); 133 | 134 | function getCash() external view returns (uint256); 135 | 136 | function accrueInterest() external returns (uint256); 137 | 138 | function seize( 139 | address liquidator, 140 | address borrower, 141 | uint256 seizeTokens 142 | ) external returns (uint256); 143 | 144 | function liquidateBorrow( 145 | address borrower, 146 | uint amount, 147 | JToken jTokenCollateral 148 | ) external returns (uint256); 149 | 150 | 151 | function mint(uint256 mintAmount) external returns (uint256); 152 | function redeem(uint256 redeemTokens) external returns (uint256); 153 | function borrow(uint256 borrowAmount) external returns (uint256); 154 | } 155 | 156 | interface JTokenNative { 157 | 158 | 159 | function transfer(address dst, uint256 amount) external returns (bool); 160 | 161 | function transferFrom( 162 | address src, 163 | address dst, 164 | uint256 amount 165 | ) external returns (bool); 166 | 167 | function approve(address spender, uint256 amount) external returns (bool); 168 | 169 | function allowance(address owner, address spender) external view returns (uint256); 170 | 171 | function balanceOf(address owner) external view returns (uint256); 172 | 173 | function balanceOfUnderlying(address owner) external returns (uint256); 174 | 175 | function getAccountSnapshot(address account) 176 | external 177 | view 178 | returns ( 179 | uint256, 180 | uint256, 181 | uint256, 182 | uint256 183 | ); 184 | 185 | function borrowRatePerSecond() external view returns (uint256); 186 | 187 | function supplyRatePerSecond() external view returns (uint256); 188 | 189 | function totalBorrowsCurrent() external returns (uint256); 190 | 191 | function borrowBalanceCurrent(address account) external returns (uint256); 192 | 193 | function borrowBalanceStored(address account) external view returns (uint256); 194 | 195 | function exchangeRateCurrent() external returns (uint256); 196 | 197 | function exchangeRateStored() external view returns (uint256); 198 | 199 | function getCash() external view returns (uint256); 200 | 201 | function accrueInterest() external returns (uint256); 202 | 203 | function seize( 204 | address liquidator, 205 | address borrower, 206 | uint256 seizeTokens 207 | ) external returns (uint256); 208 | 209 | function liquidateBorrow( 210 | address borrower, 211 | JToken jTokenCollateral 212 | ) external returns (uint256); 213 | 214 | function mint(uint256 mintAmount) external returns (uint256); 215 | function redeem(uint256 redeemTokens) external returns (uint256); 216 | function redeemNative(uint256 redeemTokens) external returns (uint256); 217 | function mintNative() external payable returns (uint256); 218 | function borrow(uint256 borrowAmount) external returns (uint256); 219 | } 220 | 221 | interface JComptroller { 222 | 223 | 224 | function enterMarkets(address[] calldata jTokens) external returns (uint256[] memory); 225 | 226 | function exitMarket(address jToken) external returns (uint256); 227 | 228 | /*** Policy Hooks ***/ 229 | 230 | function mintAllowed( 231 | address jToken, 232 | address minter, 233 | uint256 mintAmount 234 | ) external returns (uint256); 235 | 236 | function mintVerify( 237 | address jToken, 238 | address minter, 239 | uint256 mintAmount, 240 | uint256 mintTokens 241 | ) external; 242 | 243 | function redeemAllowed( 244 | address jToken, 245 | address redeemer, 246 | uint256 redeemTokens 247 | ) external returns (uint256); 248 | 249 | function redeemVerify( 250 | address jToken, 251 | address redeemer, 252 | uint256 redeemAmount, 253 | uint256 redeemTokens 254 | ) external; 255 | 256 | function borrowAllowed( 257 | address jToken, 258 | address borrower, 259 | uint256 borrowAmount 260 | ) external returns (uint256); 261 | 262 | function borrowVerify( 263 | address jToken, 264 | address borrower, 265 | uint256 borrowAmount 266 | ) external; 267 | 268 | function repayBorrowAllowed( 269 | address jToken, 270 | address payer, 271 | address borrower, 272 | uint256 repayAmount 273 | ) external returns (uint256); 274 | 275 | function repayBorrowVerify( 276 | address jToken, 277 | address payer, 278 | address borrower, 279 | uint256 repayAmount, 280 | uint256 borrowerIndex 281 | ) external; 282 | 283 | function liquidateBorrowAllowed( 284 | address jTokenBorrowed, 285 | address jTokenCollateral, 286 | address liquidator, 287 | address borrower, 288 | uint256 repayAmount 289 | ) external returns (uint256); 290 | 291 | function liquidateBorrowVerify( 292 | address jTokenBorrowed, 293 | address jTokenCollateral, 294 | address liquidator, 295 | address borrower, 296 | uint256 repayAmount, 297 | uint256 seizeTokens 298 | ) external; 299 | 300 | function seizeAllowed( 301 | address jTokenCollateral, 302 | address jTokenBorrowed, 303 | address liquidator, 304 | address borrower, 305 | uint256 seizeTokens 306 | ) external returns (uint256); 307 | 308 | function seizeVerify( 309 | address jTokenCollateral, 310 | address jTokenBorrowed, 311 | address liquidator, 312 | address borrower, 313 | uint256 seizeTokens 314 | ) external; 315 | 316 | function transferAllowed( 317 | address jToken, 318 | address src, 319 | address dst, 320 | uint256 transferTokens 321 | ) external returns (uint256); 322 | 323 | function transferVerify( 324 | address jToken, 325 | address src, 326 | address dst, 327 | uint256 transferTokens 328 | ) external; 329 | 330 | /*** Liquidity/Liquidation Calculations ***/ 331 | 332 | function liquidateCalculateSeizeTokens( 333 | address jTokenBorrowed, 334 | address jTokenCollateral, 335 | uint256 repayAmount 336 | ) external view returns (uint256, uint256); 337 | 338 | function checkMembership(address account, JToken jToken) external view returns (bool); 339 | function closeFactorMantissa() external returns (uint); 340 | function liquidationIncentiveMantissa() external returns (uint); 341 | function getAccountLiquidity(address account) external view returns (uint error, uint liquidity, uint shortfall); 342 | function isMarketListed(address market) external view returns (bool); 343 | function getAssetsIn(address account) external view returns (JToken[] memory); 344 | function getHypotheticalAccountLiquidity( 345 | address account, 346 | address jTokenModify, 347 | uint256 redeemTokens, 348 | uint256 borrowAmount 349 | ) 350 | external 351 | view 352 | returns ( 353 | uint256, 354 | uint256, 355 | uint256 356 | ); 357 | } 358 | 359 | interface PriceOracle { 360 | function getUnderlyingPrice(JToken jToken) external view returns (uint256); 361 | } 362 | -------------------------------------------------------------------------------- /tests/test_liquidator.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from brownie import ( 4 | Liquidator, 5 | TestLiquidator, 6 | Wei, 7 | accounts, 8 | chain, 9 | convert, 10 | interface, 11 | reverts, 12 | ) 13 | from brownie.exceptions import VirtualMachineError 14 | import pytest 15 | 16 | from scripts.liquidation import liquidation_parameters, LiquidationParameters 17 | 18 | 19 | WAVAX = "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7" 20 | LINK = "0x5947bb275c521040051d82396192181b413227a3" 21 | USDT = "0xc7198437980c041c805a1edcba50c1ce5db95118" 22 | USDC = "0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664" 23 | 24 | JOE_ROUTER_ADDRESS = "0x60aE616a2155Ee3d9A68541Ba4544862310933d4" 25 | JOE_COMPTROLLER_ADDRESS = "0xdc13687554205E5b89Ac783db14bb5bba4A1eDaC" 26 | 27 | JAVAX_ADDRESS = "0xC22F01ddc8010Ee05574028528614634684EC29e" 28 | JLINK_ADDRESS = "0x585E7bC75089eD111b656faA7aeb1104F5b96c15" 29 | JUSDT_ADDRESS = "0x8b650e26404AC6837539ca96812f0123601E4448" 30 | JUSDC_ADDRESS = "0xEd6AaF91a2B084bd594DBd1245be3691F9f637aC" 31 | 32 | ORACLE_ADDRESS = "0xe34309613B061545d42c4160ec4d64240b114482" 33 | 34 | 35 | @pytest.fixture 36 | def liquidator(): 37 | contract = Liquidator.deploy( 38 | JOE_ROUTER_ADDRESS, 39 | JOE_COMPTROLLER_ADDRESS, 40 | {'from': accounts[0]}) 41 | return contract 42 | 43 | 44 | @pytest.fixture 45 | def test_liquidator(): 46 | contract = TestLiquidator.deploy( 47 | JOE_ROUTER_ADDRESS, 48 | JOE_COMPTROLLER_ADDRESS, 49 | {'from': accounts[0]}) 50 | return contract 51 | 52 | 53 | @pytest.fixture 54 | def comptroller(): 55 | return interface.JComptroller(JOE_COMPTROLLER_ADDRESS) 56 | 57 | 58 | @pytest.fixture 59 | def javax(): 60 | return interface.JTokenNative(JAVAX_ADDRESS) 61 | 62 | 63 | @pytest.fixture 64 | def jlink(): 65 | return interface.JToken(JLINK_ADDRESS) 66 | 67 | 68 | @pytest.fixture 69 | def jusdt(): 70 | return interface.JToken(JUSDT_ADDRESS) 71 | 72 | 73 | @pytest.fixture 74 | def oracle(): 75 | return interface.PriceOracle(ORACLE_ADDRESS) 76 | 77 | 78 | @pytest.fixture 79 | def joe_router(): 80 | return interface.IJoeRouter02(JOE_ROUTER_ADDRESS) 81 | 82 | 83 | def test_contract_owner(liquidator): 84 | assert liquidator.owner() == accounts[0].address 85 | 86 | 87 | def test_swap_erc20_no_balance(test_liquidator): 88 | with reverts(): 89 | test_liquidator._swapERC20(LINK, USDT) 90 | 91 | 92 | def test_swap_from_native_no_balance(test_liquidator): 93 | with reverts(): 94 | test_liquidator._swapFromNative(LINK); 95 | 96 | 97 | def test_swap_from_native(test_liquidator): 98 | test_liquidator.fund({'from': accounts[0], 'amount': Wei("1 ether")}) 99 | assert test_liquidator.balance() == Wei("1 ether") 100 | assert interface.IERC20(LINK).balanceOf(test_liquidator.address) == 0 101 | test_liquidator._swapFromNative(LINK) 102 | # XXX: Get oracle rate and assert actual balance. 103 | assert interface.IERC20(LINK).balanceOf(test_liquidator.address) > 0 104 | 105 | 106 | def test_swap_erc20(test_liquidator): 107 | test_liquidator.fund({'from': accounts[0], 'amount': Wei("1 ether")}) 108 | test_liquidator._swapFromNative(LINK) 109 | 110 | test_liquidator._swapERC20(LINK, USDT) 111 | 112 | assert interface.IERC20(LINK).balanceOf(test_liquidator) == 0 113 | assert interface.IERC20(USDT).balanceOf(test_liquidator) > 0 114 | 115 | 116 | def test_swap_to_native(test_liquidator): 117 | test_liquidator.fund({'from': accounts[0], 'amount': Wei("1 ether")}) 118 | test_liquidator._swapFromNative(LINK) 119 | 120 | assert test_liquidator.balance() == 0 121 | test_liquidator._swapToNative(LINK) 122 | assert test_liquidator.balance() >= 0 123 | assert interface.IERC20(LINK).balanceOf(test_liquidator.address) == 0 124 | 125 | 126 | def test_liquidate_borrow(joe_router, comptroller, jusdt, jlink, oracle): 127 | """ 128 | This test what my learning experience for liquidating underwater accounts. 129 | 130 | It is quite long and appears complicated but is quite simple. 131 | 132 | 1. account[1] supplies some LINK and borrows max USDT 133 | 2. Fast forward the block to accrue interest and put our account underwater 134 | 3. Repay the loan from account[0] and seize LINK collateral 135 | 136 | Any extra code is just swapping AVAX for the tokens needed to perform 137 | the interactions in the above steps. 138 | """ 139 | 140 | # First we need to get some link to supply, performs a swqap 141 | # on trader joe. 142 | joe_router.swapExactAVAXForTokens( 143 | Wei("1 ether"), 144 | [WAVAX, LINK], 145 | accounts[1].address, 146 | chain.time() + 60, 147 | {'from': accounts[1], 'value': Wei("1 ether")}, 148 | ) 149 | link_balance = interface.IERC20(LINK).balanceOf(accounts[1].address) 150 | assert link_balance > 0 151 | 152 | # Now we want to supply our ERC20 link into the JLink contract 153 | # and enter the link and usdt markets. 154 | assert interface.IERC20(LINK).approve( 155 | JLINK_ADDRESS, 156 | link_balance, 157 | {'from': accounts[1]}) 158 | jlink.mint(link_balance, {'from': accounts[1]}) 159 | comptroller.enterMarkets( 160 | [JLINK_ADDRESS, JUSDT_ADDRESS], 161 | {'from': accounts[1]}) 162 | 163 | assert jlink.balanceOfUnderlying.call( 164 | accounts[1].address, 165 | {'from': accounts[1]}) >= link_balance # GTE as we may have accrued interest. 166 | assert comptroller.checkMembership(accounts[1].address, JLINK_ADDRESS) == True 167 | 168 | # Now we want to borrow USDT with our link as collateral 169 | 170 | borrow_amount = _get_max_borrow_amount( 171 | comptroller, 172 | oracle, 173 | jusdt, 174 | USDT, 175 | accounts[1]) 176 | #borrow_amount *= .9999 177 | 178 | jusdt.borrow(borrow_amount, {'from': accounts[1]}) 179 | usdt_balance = interface.IERC20(USDT).balanceOf(accounts[1].address) 180 | assert usdt_balance > 0 181 | 182 | jusdt.borrowBalanceCurrent(accounts[1].address, {'from': accounts[1]}) 183 | 184 | chain.mine(blocks=1, timestamp=chain.time() + 60*60*24*352) 185 | 186 | jusdt.borrowBalanceCurrent(accounts[1].address, {'from': accounts[1]}) 187 | 188 | err, liquidity, shortfall = comptroller.getAccountLiquidity(accounts[1].address) 189 | assert shortfall > 0, "account not in shortfall" 190 | 191 | # OK here we have a shortfall of liquidity, lets see if we can liquidate 192 | # the position. 193 | borrow_balance = jusdt.borrowBalanceCurrent.call(accounts[1].address, {'from': accounts[0]}) 194 | close_factor = comptroller.closeFactorMantissa.call({'from': accounts[0]}) / 10 ** 18 195 | repay_amount = borrow_balance * close_factor / 10 ** interface.IERC20(USDT).decimals() 196 | 197 | # Our liquidator needs this many tokens, we swap on traderjoe. 198 | joe_router.swapExactAVAXForTokens( 199 | 1, 200 | [WAVAX, USDT], 201 | accounts[0].address, 202 | chain.time() + 60, 203 | {'from': accounts[0], 'value': Wei("2 ether")}, 204 | ) 205 | usdt_balance = interface.IERC20(USDT).balanceOf(accounts[1].address) 206 | assert usdt_balance / 10** interface.IERC20(USDT).decimals() > repay_amount, "Dont have correct repay amount" 207 | 208 | # Perform the liquidation. 209 | interface.IERC20(USDT).approve(JUSDT_ADDRESS, repay_amount, {'from': accounts[0]}) 210 | # Check we have no balance before liquidate 211 | assert jlink.balanceOfUnderlying.call(accounts[0].address, {'from': accounts[0]}) == 0 212 | 213 | # Perform the liquidation. 214 | jusdt.liquidateBorrow(accounts[1].address, repay_amount, jlink, {'from': accounts[0]}) 215 | 216 | # Check to make sure account 1 has some seized link tokens. 217 | underlying = jlink.balanceOfUnderlying.call(accounts[0], {'from': accounts[0]}) 218 | assert underlying > 0, "We haven't seized any tokens." 219 | balance = jlink.balanceOf(accounts[0].address) 220 | 221 | # Finally lets redeem our seized tokens. 222 | jlink.redeem(balance, {'from': accounts[0]}) 223 | assert interface.IERC20(LINK).balanceOf(accounts[0].address) > 0 224 | assert interface.IERC20(LINK).balanceOf(accounts[0].address) > underlying # Small interest build up 225 | 226 | 227 | def _get_max_borrow_amount(comptroller, oracle, borrow_token, borrow_token_address, account): 228 | """ 229 | Get the maximum this account can borrow. 230 | """ 231 | # Get account liquidity. 232 | err, liquidity, shortfall = comptroller.getAccountLiquidity(account.address) 233 | assert err == 0, "error getting account liquidity" 234 | 235 | # Get price of borrow token. 236 | price = oracle.getUnderlyingPrice(borrow_token) 237 | return liquidity * (10 ** 18) / price 238 | 239 | 240 | def test_liquidator_contract(joe_router, comptroller, jusdt, jlink, oracle): 241 | """ 242 | Testcase for our liquidator contract. 243 | 244 | This test is largely similar to `test_liquidate_borrow` except instead 245 | of liquidating the loan with a EOA (externally owned account) we deploy 246 | our liquidator contract which used flash loans to liquidate the underwater 247 | position. Note: Normally I would make a fixture for the underwater account 248 | but in this case I am just copying and pasting the code as this will probably 249 | be one of the last tests for this bounty challenge. 250 | """ 251 | 252 | # First we need to get some link to supply, performs a swqap 253 | # on trader joe. 254 | joe_router.swapExactAVAXForTokens( 255 | 1, 256 | [WAVAX, USDT], 257 | accounts[2].address, 258 | chain.time() + 60, 259 | {'from': accounts[2], 'value': Wei("1 ether")}, 260 | ) 261 | usdt_balance = interface.IERC20(USDT).balanceOf(accounts[2].address) 262 | assert usdt_balance > 0 263 | 264 | # Now we want to supply our ERC20 link into the JLink contract 265 | # and enter the link and usdt markets. 266 | assert interface.IERC20(USDT).approve( 267 | JUSDT_ADDRESS, 268 | usdt_balance, 269 | {'from': accounts[2]}) 270 | jusdt.mint(usdt_balance, {'from': accounts[2]}) 271 | comptroller.enterMarkets( 272 | [JLINK_ADDRESS, JUSDT_ADDRESS], 273 | {'from': accounts[2]}) 274 | 275 | assert jusdt.balanceOfUnderlying.call( 276 | accounts[2].address, 277 | {'from': accounts[2]}) >= usdt_balance # GTE as we may have accrued interest. 278 | assert comptroller.checkMembership(accounts[2].address, JUSDT_ADDRESS) == True 279 | 280 | # Now we want to borrow USDT with our link as collateral 281 | 282 | borrow_amount = _get_max_borrow_amount( 283 | comptroller, 284 | oracle, 285 | jlink, 286 | LINK, 287 | accounts[2]) 288 | borrow_amount *= .999 289 | 290 | jlink.borrow(borrow_amount, {'from': accounts[2]}) 291 | link_balance = interface.IERC20(LINK).balanceOf(accounts[2].address) 292 | assert link_balance > 0 293 | 294 | jlink.borrowBalanceCurrent(accounts[2].address, {'from': accounts[2]}) 295 | 296 | chain.mine(blocks=1, timestamp=chain.time() + 60*60*24*352) 297 | 298 | jlink.borrowBalanceCurrent(accounts[2].address, {'from': accounts[2]}) 299 | 300 | err, liquidity, shortfall = comptroller.getAccountLiquidity(accounts[2].address) 301 | assert shortfall > 0, "account not in shortfall" 302 | 303 | # Now we deploy our contract and liquidate. 304 | contract = Liquidator.deploy( 305 | JOE_ROUTER_ADDRESS, 306 | JOE_COMPTROLLER_ADDRESS, 307 | {'from': accounts[0]}) 308 | 309 | # Let the contract liquidate. 310 | tx = contract.liquidateLoan( 311 | accounts[2].address, 312 | JLINK_ADDRESS, 313 | LINK, 314 | JUSDT_ADDRESS, 315 | USDT, 316 | JUSDC_ADDRESS, 317 | USDC, 318 | {'from': accounts[0]} 319 | ); 320 | # Assert we have some profit. 321 | assert interface.IERC20(USDC).balanceOf(accounts[0].address) > 0 322 | 323 | 324 | @pytest.mark.parametrize( 325 | "accounts,expected", [ 326 | ([], []), 327 | ([{'health': '0.040151181763124301', 328 | 'id': '0xd9233c98d84e50f07b122ee0de0a6a50f49127e0', 329 | 'tokens': [{'borrowBalanceUnderlying': '0', 330 | 'enteredMarket': True, 331 | 'id': '0x8b650e26404ac6837539ca96812f0123601e4448-0xd9233c98d84e50f07b122ee0de0a6a50f49127e0', 332 | 'supplyBalanceUnderlying': '0', 333 | 'symbol': 'jUSDT'}, 334 | {'borrowBalanceUnderlying': '0', 335 | 'enteredMarket': True, 336 | 'id': '0x929f5cab61dfec79a5431a7734a68d714c4633fa-0xd9233c98d84e50f07b122ee0de0a6a50f49127e0', 337 | 'supplyBalanceUnderlying': '0', 338 | 'symbol': 'jWETH'}, 339 | {'borrowBalanceUnderlying': '0', 340 | 'enteredMarket': True, 341 | 'id': '0xc988c170d0e38197dc634a45bf00169c7aa7ca19-0xd9233c98d84e50f07b122ee0de0a6a50f49127e0', 342 | 'supplyBalanceUnderlying': '0', 343 | 'symbol': 'jDAI'}, 344 | {'borrowBalanceUnderlying': '8479.700995064730131663373878343748', 345 | 'enteredMarket': True, 346 | 'id': '0xed6aaf91a2b084bd594dbd1245be3691f9f637ac-0xd9233c98d84e50f07b122ee0de0a6a50f49127e0', 347 | 'supplyBalanceUnderlying': '10940.151993880858163609064043', 348 | 'symbol': 'jUSDC'}], 349 | 'totalBorrowValueInUSD': '8414.278374', 350 | 'totalCollateralValueInUSD': '8752.1215944'} 351 | ], [ 352 | LiquidationParameters( 353 | convert.to_address('0xd9233c98d84e50f07b122ee0de0a6a50f49127e0'), 354 | convert.to_address('0xed6aaf91a2b084bd594dbd1245be3691f9f637ac'), 355 | convert.to_address('0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664'), 356 | convert.to_address('0xed6aaf91a2b084bd594dbd1245be3691f9f637ac'), 357 | convert.to_address('0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664'), 358 | ANY, 359 | ANY, 360 | ) 361 | ]) 362 | ]) 363 | def test_liquidation_params(accounts, expected): 364 | """ 365 | Test for our liquidation algo. 366 | """ 367 | assert list(liquidation_parameters(accounts)) == expected 368 | --------------------------------------------------------------------------------