├── .secret.ts.sample ├── .solhint.json ├── .gitignore ├── .prettierrc ├── searcher ├── flashbotsUserStat.ts ├── checkLoans.ts ├── sethSearcher.ts ├── susdSearcher.ts ├── flashbotsBase.ts ├── loaners.ts └── mempoolSearcher.ts ├── scripts ├── deploy.ts └── chi.ts ├── README.md ├── tsconfig.json ├── hardhat.config.ts ├── package.json ├── contracts ├── DydxHelper.sol └── SnxLiquidator.sol └── test └── LiquidationTest.ts /.secret.ts.sample: -------------------------------------------------------------------------------- 1 | export default { 2 | private: 'PRIVATE_KEY', 3 | }; 4 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "rules": { 4 | "prettier/prettier": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | 4 | #Hardhat files 5 | cache 6 | artifacts 7 | typechain 8 | 9 | #Secret key 10 | .secret.ts 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "printWidth": 120, 7 | "tabWidth": 4, 8 | "useTabs": false, 9 | "bracketSpacing": false, 10 | "explicitTypes": "always" 11 | } 12 | } 13 | ], 14 | "singleQuote": true 15 | } 16 | -------------------------------------------------------------------------------- /searcher/flashbotsUserStat.ts: -------------------------------------------------------------------------------- 1 | import { createFlashbotsProvider } from './flashbotsBase'; 2 | 3 | async function main() { 4 | const flashbotsProvider = await createFlashbotsProvider(); 5 | const stat = await flashbotsProvider.getUserStats(); 6 | console.log(stat); 7 | } 8 | 9 | main() 10 | .then(() => process.exit(0)) 11 | .catch((error) => { 12 | console.error(error); 13 | process.exit(1); 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | 3 | async function main() { 4 | const factory = await ethers.getContractFactory('SnxLiquidator'); 5 | const snxLiquidator = await factory.deploy(); 6 | 7 | await snxLiquidator.deployed(); 8 | console.log(`Contract deployed to ${snxLiquidator.address}`); 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch((err) => { 14 | console.error(err); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snx-liquidator 2 | 3 | This is a contract & bot repo for a one-shot MEV: [snx-trial-loans](https://sips.synthetix.io/sips/sip-142/) 4 | 5 | It uses flashbots & block-native mempool listener to liquidate loans in SNX. 6 | 7 | Though I failed the competition, but I think it might be helpful for others who want to get into the field of MEV battle ground. 8 | 9 | PS: The reason I did not win: 10 | - miner bribe too low, others give more miner bribe to the flashbots miner 11 | - too many TXs in the bundle, miners would just ignore those bundles with too many txs coz it might take too much time for them to simulate the Txs 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "typeRoots": [ 9 | "./typechain", 10 | "./node_modules/@types" 11 | ], 12 | "types": [ 13 | "@nomiclabs/hardhat-ethers", 14 | "@nomiclabs/hardhat-waffle" 15 | ] 16 | }, 17 | "include": [ 18 | "./scripts", 19 | "./test", 20 | "./searcher" 21 | ], 22 | "files": [ 23 | "./hardhat.config.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /scripts/chi.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { SnxLiquidator } from '../typechain/SnxLiquidator'; 3 | 4 | const CONTRACT_ADDR = '0xb0C352225B161Da1Ba92b7d60Db3c26bF24c1Bb5'; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | 9 | const factory = await ethers.getContractFactory('SnxLiquidator'); 10 | const snxLiquidator = factory 11 | .attach(CONTRACT_ADDR) 12 | .connect(signer) as SnxLiquidator; 13 | 14 | const tx = await snxLiquidator.mintCHI('200'); 15 | tx.wait(1); 16 | console.log(`CHI minted, tx hash ${tx.hash}`); 17 | } 18 | 19 | main() 20 | .then(() => process.exit(0)) 21 | .catch((err) => { 22 | console.error(err); 23 | process.exit(1); 24 | }); 25 | -------------------------------------------------------------------------------- /searcher/checkLoans.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Contract } from 'ethers'; 2 | import { susdCollateral, sethCollateral } from './flashbotsBase'; 3 | import { susdLoaners, sethLoaners } from './loaners'; 4 | 5 | async function main() { 6 | console.log('Check susd loaners'); 7 | await checkLoans(susdLoaners, susdCollateral); 8 | console.log('Check seth loaners'); 9 | await checkLoans(sethLoaners, sethCollateral); 10 | } 11 | 12 | async function checkLoans(loaners: Array, contract: Contract) { 13 | for (const loaner of loaners) { 14 | const [, , , , timeClosed] = await contract.getLoan( 15 | loaner.account, 16 | loaner.loanID 17 | ); 18 | console.log( 19 | `account: ${loaner.account}, closed: ${(timeClosed as BigNumber).eq('0')}` 20 | ); 21 | } 22 | } 23 | 24 | main() 25 | .then(() => process.exit(0)) 26 | .catch((error) => { 27 | console.error(error); 28 | process.exit(1); 29 | }); 30 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config'; 2 | import '@typechain/hardhat'; 3 | import '@nomiclabs/hardhat-waffle'; 4 | 5 | import operator from './.secret'; 6 | 7 | const config: HardhatUserConfig = { 8 | solidity: { version: '0.8.4' }, 9 | networks: { 10 | hardhat: { 11 | // loggingEnabled: true, 12 | forking: { 13 | url: 'https://eth-mainnet.alchemyapi.io/v2/iAHwO4-koDDdXeemLhT-4i8jsx8phFnb', 14 | enabled: true, 15 | blockNumber: 12697464, 16 | }, 17 | }, 18 | mainnet: { 19 | url: 'https://mainnet.infura.io/v3/3a57292f72e4472b8ac896816a27d51f', 20 | accounts: [operator.private], 21 | }, 22 | rinkeby: { 23 | url: 'https://rinkeby.infura.io/v3/b042a80255fa41e5a2f22f53e3190b44', 24 | accounts: [operator.private], 25 | }, 26 | }, 27 | mocha: { 28 | timeout: 300000, 29 | }, 30 | }; 31 | 32 | /** 33 | * @type import('hardhat/config').HardhatUserConfig 34 | */ 35 | module.exports = config; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snx-liquidator", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Penghui Liao ", 6 | "license": "MIT", 7 | "scripts": { 8 | "seth-searcher": "ts-node ./searcher/susdSearcher.ts", 9 | "susd-searcher": "ts-node ./searcher/sethSearcher.ts", 10 | "mempool-searcher": "ts-node ./searcher/mempoolSearcher.ts", 11 | "flashbots-user-stat": "ts-node ./searcher/flashbotsUserStat.ts" 12 | }, 13 | "dependencies": { 14 | "@uniswap/v3-core": "^1.0.0", 15 | "@uniswap/v3-periphery": "^1.1.0" 16 | }, 17 | "devDependencies": { 18 | "@flashbots/ethers-provider-bundle": "^0.3.2", 19 | "@nomiclabs/hardhat-ethers": "^2.0.2", 20 | "@nomiclabs/hardhat-waffle": "^2.0.1", 21 | "@typechain/ethers-v5": "^7.0.1", 22 | "@typechain/hardhat": "^2.1.0", 23 | "@types/chai": "^4.2.19", 24 | "@types/mocha": "^8.2.2", 25 | "@types/node": "^15.12.4", 26 | "@types/ws": "^7.4.5", 27 | "bnc-sdk": "^3.4.0", 28 | "chai": "^4.3.4", 29 | "ethereum-waffle": "^3.4.0", 30 | "ethers": "^5.3.1", 31 | "hardhat": "^2.4.1", 32 | "prettier": "^2.3.1", 33 | "prettier-plugin-solidity": "^1.0.0-beta.13", 34 | "ts-node": "^10.0.0", 35 | "typechain": "^5.1.0", 36 | "typescript": "^4.3.4", 37 | "ws": "^7.5.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/DydxHelper.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | // These definitions are taken from across multiple dydx contracts, and are 5 | // limited to just the bare minimum necessary to make flash loans work. 6 | library Types { 7 | enum AssetDenomination { Wei, Par } 8 | enum AssetReference { Delta, Target } 9 | struct AssetAmount { 10 | bool sign; 11 | AssetDenomination denomination; 12 | AssetReference ref; 13 | uint256 value; 14 | } 15 | } 16 | 17 | library Account { 18 | struct Info { 19 | address owner; 20 | uint256 number; 21 | } 22 | } 23 | 24 | library Actions { 25 | enum ActionType { 26 | Deposit, // supply tokens 27 | Withdraw, // borrow tokens 28 | Transfer, // transfer balance between accounts 29 | Buy, // buy an amount of some token (externally) 30 | Sell, // sell an amount of some token (externally) 31 | Trade, // trade tokens against another account 32 | Liquidate, // liquidate an undercollateralized or expiring account 33 | Vaporize, // use excess tokens to zero-out a completely negative account 34 | Call // send arbitrary data to an address 35 | } 36 | struct ActionArgs { 37 | ActionType actionType; 38 | uint256 accountId; 39 | Types.AssetAmount amount; 40 | uint256 primaryMarketId; 41 | uint256 secondaryMarketId; 42 | address otherAddress; 43 | uint256 otherAccountId; 44 | bytes data; 45 | } 46 | } 47 | 48 | interface ISoloMargin { 49 | function operate(Account.Info[] memory accounts, Actions.ActionArgs[] memory actions) external; 50 | } 51 | 52 | // The interface for a contract to be callable after receiving a flash loan 53 | interface IDydxCallee { 54 | function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external; 55 | } 56 | -------------------------------------------------------------------------------- /searcher/sethSearcher.ts: -------------------------------------------------------------------------------- 1 | import { sethLoaners } from './loaners'; 2 | import { 3 | provider, 4 | createFlashbotsProvider, 5 | getBundles, 6 | sethCollateral, 7 | } from './flashbotsBase'; 8 | 9 | async function main() { 10 | const flashbotsProvider = await createFlashbotsProvider(); 11 | 12 | const [signedTxs, revertingTxHashes] = await getBundles( 13 | sethLoaners, 14 | flashbotsProvider 15 | ); 16 | 17 | let opened = false; 18 | 19 | provider.on('block', async (blockNumber) => { 20 | console.log(`Block number: ${blockNumber}`); 21 | 22 | if (!opened) { 23 | const snxOpened = await sethCollateral.loanLiquidationOpen(); 24 | console.log(`SETH liquidation opened: ${snxOpened}`); 25 | opened = snxOpened; 26 | } 27 | 28 | if (opened) { 29 | // const simulation = await flashbotsProvider.simulate( 30 | // signedTxs, 31 | // blockNumber + 1 32 | // ); 33 | // // Using TypeScript discrimination 34 | // if ('error' in simulation) { 35 | // console.log(`Simulation Error: ${simulation.error.message}`); 36 | // } else { 37 | // console.log(`Simulation Success:`); 38 | // } 39 | 40 | const bundleSubmission = await flashbotsProvider.sendRawBundle( 41 | signedTxs, 42 | blockNumber + 1, 43 | { revertingTxHashes } 44 | ); 45 | console.log(`bundle submitted, waiting`); 46 | if ('error' in bundleSubmission) { 47 | throw new Error(bundleSubmission.error.message); 48 | } 49 | 50 | const waitResponse = await bundleSubmission.wait(); 51 | console.log(`Response: ${waitResponse}`); 52 | 53 | if (waitResponse === 0) { 54 | console.log('Bundle handled successfully'); 55 | process.exit(0); 56 | } else { 57 | } 58 | } 59 | }); 60 | } 61 | 62 | main() 63 | .then() 64 | .catch((error) => { 65 | console.error(error); 66 | process.exit(1); 67 | }); 68 | -------------------------------------------------------------------------------- /searcher/susdSearcher.ts: -------------------------------------------------------------------------------- 1 | import { susdLoaners } from './loaners'; 2 | import { 3 | provider, 4 | createFlashbotsProvider, 5 | getBundles, 6 | susdCollateral, 7 | } from './flashbotsBase'; 8 | 9 | async function main() { 10 | const flashbotsProvider = await createFlashbotsProvider(); 11 | 12 | const [signedTxs, revertingTxHashes] = await getBundles( 13 | susdLoaners, 14 | flashbotsProvider 15 | ); 16 | 17 | let opened = false; 18 | 19 | provider.on('block', async (blockNumber) => { 20 | console.log(`Block number: ${blockNumber}`); 21 | 22 | if (!opened) { 23 | const snxOpened = await susdCollateral.loanLiquidationOpen(); 24 | console.log(`SUSD liquidation opened: ${snxOpened}`); 25 | opened = snxOpened; 26 | } 27 | 28 | if (opened) { 29 | // const simulation = await flashbotsProvider.simulate( 30 | // signedTxs, 31 | // blockNumber + 1 32 | // ); 33 | // // Using TypeScript discrimination 34 | // if ('error' in simulation) { 35 | // console.log(`Simulation Error: ${simulation.error.message}`); 36 | // } else { 37 | // console.log(`Simulation Success:`); 38 | // } 39 | 40 | const bundleSubmission = await flashbotsProvider.sendRawBundle( 41 | signedTxs, 42 | blockNumber + 1, 43 | { revertingTxHashes } 44 | ); 45 | console.log(`bundle submitted, waiting`); 46 | if ('error' in bundleSubmission) { 47 | throw new Error(bundleSubmission.error.message); 48 | } 49 | 50 | const waitResponse = await bundleSubmission.wait(); 51 | console.log(`Response: ${waitResponse}`); 52 | 53 | if (waitResponse === 0) { 54 | console.log('Bundle handled successfully'); 55 | process.exit(0); 56 | } else { 57 | } 58 | } 59 | }); 60 | } 61 | 62 | main() 63 | .then() 64 | .catch((error) => { 65 | console.error(error); 66 | process.exit(1); 67 | }); 68 | -------------------------------------------------------------------------------- /searcher/flashbotsBase.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { providers, Wallet } from 'ethers'; 3 | import { keccak256 } from 'ethers/lib/utils'; 4 | import { 5 | FlashbotsBundleProvider, 6 | FlashbotsBundleTransaction, 7 | } from '@flashbots/ethers-provider-bundle'; 8 | 9 | import { SnxLiquidator } from '../typechain/SnxLiquidator'; 10 | import operator from '../.secret'; 11 | 12 | // Standard json rpc provider directly from ethers.js (NOT Flashbots) 13 | export const provider = new providers.WebSocketProvider( 14 | 'wss://mainnet.infura.io/ws/v3/3a57292f72e4472b8ac896816a27d51f' 15 | ); 16 | 17 | // deployed liquidator contract address 18 | export const contractAddr = '0xb0C352225B161Da1Ba92b7d60Db3c26bF24c1Bb5'; 19 | 20 | const loanABI = [ 21 | 'function loanLiquidationOpen() external view returns(bool)', 22 | 'function getLoan(address _account, uint256 _loanID) external view returns (address,uint256,uint256,uint256,uint256,uint256,uint256, uint256)', 23 | ]; 24 | 25 | export const susdCollateralAddr = '0xfED77055B40d63DCf17ab250FFD6948FBFF57B82'; 26 | export const susdCollateral = new ethers.Contract( 27 | susdCollateralAddr, 28 | loanABI, 29 | provider 30 | ); 31 | 32 | export const sethCollateralAddr = '0x7133afF303539b0A4F60Ab9bd9656598BF49E272'; 33 | export const sethCollateral = new ethers.Contract( 34 | sethCollateralAddr, 35 | loanABI, 36 | provider 37 | ); 38 | 39 | // flashtbots relay signer 40 | const flashbotsSigner = new Wallet(operator.flashBotPrivate); 41 | 42 | export async function createFlashbotsProvider(): Promise { 43 | return await FlashbotsBundleProvider.create(provider, flashbotsSigner); 44 | } 45 | 46 | // arbitrage signer 47 | const signer = Wallet.createRandom().connect(provider); 48 | 49 | export async function getBundles( 50 | loaners: Array, 51 | flashbotsProvider: FlashbotsBundleProvider 52 | ): Promise<[Array, Array]> { 53 | const factory = await ethers.getContractFactory('SnxLiquidator'); 54 | const liquidator = factory.attach(contractAddr) as SnxLiquidator; 55 | 56 | let bundles = new Array(); 57 | for (const loaner of loaners) { 58 | const tx = await liquidator 59 | .connect(signer) 60 | .populateTransaction.liquidate( 61 | loaner.account, 62 | loaner.loanID, 63 | loaner.loanType, 64 | loaner.minerBp, 65 | { 66 | gasPrice: 0, 67 | gasLimit: 1500000, 68 | } 69 | ); 70 | bundles.push({ signer: signer, transaction: tx }); 71 | } 72 | const signedTxs = await flashbotsProvider.signBundle(bundles); 73 | const revertingTxHashes = signedTxs.map((v) => keccak256(v)); 74 | return [signedTxs, revertingTxHashes]; 75 | } 76 | -------------------------------------------------------------------------------- /searcher/loaners.ts: -------------------------------------------------------------------------------- 1 | export const susdLoaners = [ 2 | { 3 | account: '0x45899a8104CDa54deaBaDDA505f0bBA68223F631', 4 | collateralAmount: '150.0', 5 | loanAmount: '125296.15022400001', 6 | loanID: 283, 7 | loanType: 0, 8 | minerBp: '2000', 9 | }, 10 | { 11 | account: '0x6235928f952Cd561B906D42b02Fe850bE2180012', 12 | collateralAmount: '150.0', 13 | loanAmount: '101166.93680324999', 14 | loanID: 228, 15 | loanType: 0, 16 | minerBp: '2000', 17 | }, 18 | { 19 | account: '0x569680033Cf81D379E24e0553230aB444Fe10559', 20 | collateralAmount: '15.365351905875884', 21 | loanAmount: '15015.439600000002', 22 | loanID: 305, 23 | loanType: 0, 24 | minerBp: '5000', 25 | }, 26 | { 27 | account: '0xC70816Aa7EaC61bBd59D9aE5D6b01392a466A886', 28 | collateralAmount: '9.117489916819968', 29 | loanAmount: '6000.0', 30 | loanID: 240, 31 | loanType: 0, 32 | minerBp: '7000', 33 | }, 34 | 35 | { 36 | account: '0xEE58FCeE1a11D0EC79dDE3116688ca3efBE6B7cC', 37 | collateralAmount: '10.0', 38 | loanAmount: '2000.58078385', 39 | loanID: 61, 40 | loanType: 0, 41 | minerBp: '9000', 42 | }, 43 | { 44 | account: '0xf7ECaE6F035EA4927FDE97FaA679b5e224afb169', 45 | collateralAmount: '0.000000000000000013', 46 | loanAmount: '1821.257169545740921384', 47 | loanID: 292, 48 | loanType: 0, 49 | minerBp: '9500', 50 | }, 51 | { 52 | account: '0x4CCc839FEd930230E5fb52C0293d4633a356A27e', 53 | collateralAmount: '9.0', 54 | loanAmount: '1572.837898275', 55 | loanID: 9, 56 | loanType: 0, 57 | minerBp: '9500', 58 | }, 59 | { 60 | account: '0x9b29B87B8428FAb4228a16d8d38A6482cB7e68eb', 61 | collateralAmount: '7.0', 62 | loanAmount: '1231.6850000000002', 63 | loanID: 34, 64 | loanType: 0, 65 | minerBp: '9500', 66 | }, 67 | { 68 | account: '0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000', 69 | collateralAmount: '9.5', 70 | loanAmount: '1770.705', 71 | loanID: 42, 72 | loanType: 0, 73 | minerBp: '9500', 74 | }, 75 | { 76 | account: '0x69Eb40B6E9ea1953d4F5d28667Cc7A1B773be68c', 77 | collateralAmount: '1.5209920349076893', 78 | loanAmount: '1000.0', 79 | loanID: 239, 80 | loanType: 0, 81 | minerBp: '9500', 82 | }, 83 | 84 | { 85 | account: '0x81ADa00e9a9DF3d84fFC781349BD3720f84C61E6', 86 | collateralAmount: '1.85', 87 | loanAmount: '1539.28325', 88 | loanID: 253, 89 | loanType: 0, 90 | minerBp: '9500', 91 | }, 92 | { 93 | account: '0x7EFE3AC6Ec0Ff5b6136766aC79a97c1e9d8fd585', 94 | collateralAmount: '1.873007972328334074', 95 | loanAmount: '1941.354279095112677944', 96 | loanID: 274, 97 | loanType: 0, 98 | minerBp: '9500', 99 | }, 100 | ]; 101 | 102 | export const sethLoaners = [ 103 | { 104 | account: '0x820B24277A86fAc14ef5150c58B1815Cf9A3Cf46', 105 | collateralAmount: '10.0', 106 | loanAmount: '8.0', 107 | loanID: 33, 108 | loanType: 1, 109 | minerBp: '2000', 110 | }, 111 | { 112 | account: '0x88C407C053bD2F434923ae13CED7213B4d2F8aE8', 113 | collateralAmount: '6.0', 114 | loanAmount: '4.8', 115 | loanID: 56, 116 | loanType: 1, 117 | minerBp: '2500', 118 | }, 119 | { 120 | account: '0x5c46DeeA1783f1d7e9579B963f2021AC1B5BFE7b', 121 | collateralAmount: '2.0', 122 | loanAmount: '1.6', 123 | loanID: 6, 124 | loanType: 1, 125 | minerBp: '3500', 126 | }, 127 | { 128 | account: '0x6899f448072222c98E65ce3f29d9CcB92C739ad1', 129 | collateralAmount: '1.0', 130 | loanAmount: '0.8', 131 | loanID: 98, 132 | loanType: 1, 133 | minerBp: '6000', 134 | }, 135 | { 136 | account: '0x6899f448072222c98E65ce3f29d9CcB92C739ad1', 137 | collateralAmount: '1.0', 138 | loanAmount: '0.8', 139 | loanID: 99, 140 | loanType: 1, 141 | minerBp: '6000', 142 | }, 143 | { 144 | account: '0x6899f448072222c98E65ce3f29d9CcB92C739ad1', 145 | collateralAmount: '1.0', 146 | loanAmount: '0.8', 147 | loanID: 100, 148 | loanType: 1, 149 | minerBp: '6000', 150 | }, 151 | { 152 | account: '0x6899f448072222c98E65ce3f29d9CcB92C739ad1', 153 | collateralAmount: '1.0', 154 | loanAmount: '0.8', 155 | loanID: 101, 156 | loanType: 1, 157 | minerBp: '6000', 158 | }, 159 | { 160 | account: '0x6899f448072222c98E65ce3f29d9CcB92C739ad1', 161 | collateralAmount: '1.0', 162 | loanAmount: '0.8', 163 | loanID: 102, 164 | loanType: 1, 165 | minerBp: '6000', 166 | }, 167 | { 168 | account: '0x3Be2e55CEFa413aff52aDC2e94892Fd1478D41BB', 169 | collateralAmount: '1.0', 170 | loanAmount: '0.8', 171 | loanID: 104, 172 | loanType: 1, 173 | minerBp: '6000', 174 | }, 175 | ]; 176 | -------------------------------------------------------------------------------- /searcher/mempoolSearcher.ts: -------------------------------------------------------------------------------- 1 | import BlocknativeSdk from 'bnc-sdk'; 2 | import { EthereumTransactionData } from 'bnc-sdk/dist/types/src/interfaces'; 3 | import WebSocket from 'ws'; 4 | import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle'; 5 | import { encode } from 'rlp'; 6 | import { BigNumber } from 'ethers'; 7 | import { hexStripZeros } from 'ethers/lib/utils'; 8 | 9 | import { sethLoaners, susdLoaners } from './loaners'; 10 | import { 11 | provider, 12 | createFlashbotsProvider, 13 | getBundles, 14 | susdCollateralAddr, 15 | sethCollateralAddr, 16 | } from './flashbotsBase'; 17 | 18 | const snxDAO = '0xEb3107117FEAd7de89Cd14D463D340A2E6917769'; 19 | 20 | function toHex(n: any): string { 21 | return hexStripZeros(BigNumber.from(n)._hex); 22 | } 23 | 24 | function constructSignedTx(tx: EthereumTransactionData): string { 25 | const params = [ 26 | toHex(tx.nonce), 27 | toHex(tx.gasPrice), 28 | toHex(tx.gas), 29 | tx.to, 30 | toHex(tx.value), 31 | tx.input, 32 | tx.v, 33 | tx.r, 34 | tx.s, 35 | ]; 36 | return '0x' + encode(params).toString('hex'); 37 | } 38 | 39 | async function trySubmitBundlesWithSnxTx( 40 | flashbotsProvider: FlashbotsBundleProvider, 41 | bundle: Array, 42 | revertingTxHashes: Array, 43 | snxTx: string, 44 | blockNumber: number 45 | ) { 46 | // Insert snx tx to the begining 47 | bundle.unshift(snxTx); 48 | 49 | // try 3 blocks 50 | for (let i = blockNumber + 1; i <= blockNumber + 3; i++) { 51 | console.log(`Try submit bundle on block ${i}`); 52 | 53 | const bundleSubmission = await flashbotsProvider.sendRawBundle(bundle, i, { 54 | revertingTxHashes, 55 | }); 56 | console.log('bundle submitted, waiting'); 57 | if ('error' in bundleSubmission) { 58 | console.error( 59 | 'Bundle submission error: ', 60 | bundleSubmission.error.message 61 | ); 62 | continue; 63 | } 64 | 65 | const waitResponse = await bundleSubmission.wait(); 66 | console.log('Response code: ', waitResponse); 67 | if (waitResponse === 0) { 68 | console.log('Bundle hanlded successfully'); 69 | break; 70 | } 71 | } 72 | } 73 | 74 | async function main() { 75 | const options = { 76 | dappId: '85c6c02a-2df3-4758-980a-7143da2ae777', 77 | networkId: 1, 78 | ws: WebSocket, 79 | name: 'Snx DAO monitor', 80 | onerror: (error: any) => { 81 | console.log(error); 82 | }, 83 | }; 84 | const blocknative = new BlocknativeSdk(options); 85 | blocknative.configuration({ scope: snxDAO, watchAddress: true }); 86 | const { emitter } = blocknative.account(snxDAO); 87 | 88 | const flashbotsProvider = await createFlashbotsProvider(); 89 | 90 | // prepare liquidation tx bundles 91 | const [susdSignedTxs, susdRevertingTxHashes] = await getBundles( 92 | susdLoaners, 93 | flashbotsProvider 94 | ); 95 | const [sethSignedTxs, sethRevertingTxHashes] = await getBundles( 96 | sethLoaners, 97 | flashbotsProvider 98 | ); 99 | 100 | // refresh block number 101 | let blockNumber = await provider.getBlockNumber(); 102 | provider.on('block', async (_blockNumber) => { 103 | console.log(`Block number: ${_blockNumber}`); 104 | blockNumber = _blockNumber; 105 | }); 106 | 107 | // blindly submit for SUSD and SETH incase the tx simulation lantency too high 108 | emitter.on('txPool', (tx) => { 109 | tx = tx as EthereumTransactionData; 110 | console.log('Tx hash:', tx.hash); 111 | const signedSnxTx = constructSignedTx(tx); 112 | 113 | (async function () { 114 | await Promise.all([ 115 | trySubmitBundlesWithSnxTx( 116 | flashbotsProvider, 117 | susdSignedTxs, 118 | susdRevertingTxHashes, 119 | signedSnxTx, 120 | blockNumber 121 | ), 122 | trySubmitBundlesWithSnxTx( 123 | flashbotsProvider, 124 | sethSignedTxs, 125 | sethRevertingTxHashes, 126 | signedSnxTx, 127 | blockNumber 128 | ), 129 | ]); 130 | })().catch((e) => console.error(e)); 131 | }); 132 | 133 | // submit for SUSD or SETH by comparing internal callee address 134 | emitter.on('txPoolSimulation', (tx) => { 135 | tx = tx as EthereumTransactionData; 136 | console.log('Tx hash:', tx.hash); 137 | const signedSnxTx = constructSignedTx(tx); 138 | for (const interCall of (tx as any).internalTransactions) { 139 | let bundle: Array, revertingHashes: Array; 140 | 141 | switch (interCall.to.toLowerCase()) { 142 | case susdCollateralAddr.toLowerCase(): 143 | bundle = susdSignedTxs; 144 | revertingHashes = susdRevertingTxHashes; 145 | break; 146 | case sethCollateralAddr.toLowerCase(): 147 | bundle = sethSignedTxs; 148 | revertingHashes = sethRevertingTxHashes; 149 | break; 150 | default: 151 | continue; 152 | } 153 | 154 | (async function () { 155 | await trySubmitBundlesWithSnxTx( 156 | flashbotsProvider, 157 | bundle, 158 | revertingHashes, 159 | signedSnxTx, 160 | blockNumber 161 | ); 162 | })().catch((e) => console.error(e)); 163 | } 164 | }); 165 | } 166 | 167 | main() 168 | .then() 169 | .catch((error) => { 170 | console.error(error); 171 | process.exit(1); 172 | }); 173 | -------------------------------------------------------------------------------- /test/LiquidationTest.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import { BigNumber, Contract, Wallet } from 'ethers'; 3 | import { ethers, network, waffle } from 'hardhat'; 4 | import { expect } from 'chai'; 5 | import chai from 'chai'; 6 | import { solidity } from 'ethereum-waffle'; 7 | chai.use(solidity); 8 | 9 | import { SnxLiquidator } from '../typechain/SnxLiquidator'; 10 | 11 | describe('SnxLiquidation', () => { 12 | let snxLiquidator: SnxLiquidator; 13 | let susdLoaner: Contract; 14 | let sethLoaner: Contract; 15 | let chi: Contract; 16 | let signer: SignerWithAddress; 17 | const provider = waffle.provider; 18 | 19 | const loanABI = [ 20 | 'function loanLiquidationOpen() external view returns(bool)', 21 | ]; 22 | const susdLoanAddr = '0xfED77055B40d63DCf17ab250FFD6948FBFF57B82'; 23 | const sethLoanAddr = '0x7133afF303539b0A4F60Ab9bd9656598BF49E272'; 24 | 25 | const chiABI = [ 26 | 'function balanceOf(address account) external view returns (uint256)', 27 | ]; 28 | const chiAddr = '0x0000000000004946c0e9F43F4Dee607b0eF1fA1c'; 29 | 30 | before(async () => { 31 | [signer] = await ethers.getSigners(); 32 | 33 | chi = new ethers.Contract(chiAddr, chiABI, provider); 34 | 35 | const SnxLiquidatorFactory = await ethers.getContractFactory( 36 | 'SnxLiquidator' 37 | ); 38 | snxLiquidator = (await SnxLiquidatorFactory.deploy()) as SnxLiquidator; 39 | const tx = await snxLiquidator.mintCHI('200'); 40 | const receipt = await tx.wait(1); 41 | console.log('Minting CHI gas used: ', receipt.gasUsed.toString()); 42 | 43 | // Set loanLiquidationOpen to true so we can mock liquidate 44 | await network.provider.send('hardhat_setStorageAt', [ 45 | susdLoanAddr, 46 | '0xf', 47 | '0x0000000000000000000000000000000000000000000000000000000000000001', 48 | ]); 49 | await network.provider.send('hardhat_setStorageAt', [ 50 | sethLoanAddr, 51 | '0xf', 52 | '0x0000000000000000000000000000000000000000000000000000000000000001', 53 | ]); 54 | 55 | susdLoaner = new ethers.Contract(susdLoanAddr, loanABI, provider); 56 | sethLoaner = new ethers.Contract(sethLoanAddr, loanABI, provider); 57 | }); 58 | 59 | it('Set the loanLiquidationOpen to true', async () => { 60 | let liquidationOpened = await susdLoaner.loanLiquidationOpen(); 61 | expect(liquidationOpened).eq(true, 'SUSD liquidation not opened'); 62 | 63 | liquidationOpened = await sethLoaner.loanLiquidationOpen(); 64 | expect(liquidationOpened).eq(true, 'SETH liquidation not opened'); 65 | }); 66 | 67 | it('Owner is set correctly', async () => { 68 | const owner = await snxLiquidator.owner(); 69 | expect(owner).eq(signer.address); 70 | }); 71 | 72 | it('Liquidate on susd with bribe', async () => { 73 | const randomSigner = Wallet.createRandom().connect(waffle.provider); 74 | snxLiquidator.connect(randomSigner); 75 | await network.provider.send('hardhat_setBalance', [ 76 | await randomSigner.getAddress(), 77 | '0xa', 78 | ]); 79 | 80 | const blockNumber = await provider.getBlockNumber(); 81 | const block = await provider.getBlock(blockNumber); 82 | const minerBalanceBefore = await provider.getBalance(block.miner); 83 | 84 | const balanceBefore = await signer.getBalance(); 85 | const tx = await snxLiquidator.liquidate( 86 | '0x45899a8104CDa54deaBaDDA505f0bBA68223F631', 87 | 283, 88 | 0, 89 | '2000', 90 | { gasPrice: '0' } 91 | ); 92 | const gasUsed = (await tx.wait(1)).gasUsed; 93 | console.log(`Gas used: ${gasUsed}`); 94 | 95 | const balanceAfter = await signer.getBalance(); 96 | const minerBalanceAfter = await provider.getBalance(block.miner); 97 | 98 | console.log('Block number: ', await provider.getBlockNumber()); 99 | 100 | expect(balanceAfter).gt(balanceBefore); 101 | console.log( 102 | 'Profit: ', 103 | ethers.utils.formatEther(balanceAfter.sub(balanceBefore)) 104 | ); 105 | 106 | expect(minerBalanceAfter).gt( 107 | minerBalanceBefore.add(ethers.utils.parseEther('2')) 108 | ); 109 | const minerProfit = minerBalanceAfter 110 | .sub(minerBalanceBefore) 111 | .sub(ethers.utils.parseEther('2')); 112 | console.log('Miner profit: ', ethers.utils.formatEther(minerProfit)); 113 | const avgGasPrice = minerProfit.div(gasUsed); 114 | console.log( 115 | 'Avg gas price in gwei: ', 116 | ethers.utils.formatUnits(avgGasPrice, 'gwei') 117 | ); 118 | 119 | // withdraw susd 120 | const withdrawTx = await snxLiquidator.withdraw( 121 | '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51' 122 | ); 123 | const receipt = await withdrawTx.wait(1); 124 | expect(receipt.status).eq(1); 125 | }); 126 | 127 | it('Liquidate on seth with bribe', async () => { 128 | const blockNumber = await provider.getBlockNumber(); 129 | const block = await provider.getBlock(blockNumber); 130 | const minerBalanceBefore = await provider.getBalance(block.miner); 131 | 132 | console.log('Block number: ', blockNumber); 133 | 134 | const balanceBefore = await signer.getBalance(); 135 | const tx = await snxLiquidator.liquidate( 136 | '0x820B24277A86fAc14ef5150c58B1815Cf9A3Cf46', 137 | 33, 138 | 1, 139 | '1500', 140 | { gasPrice: '0' } 141 | ); 142 | const gasUsed = (await tx.wait(1)).gasUsed; 143 | console.log(`Gas used: ${gasUsed}`); 144 | 145 | const balanceAfter = await signer.getBalance(); 146 | const minerBalanceAfter = await provider.getBalance(block.miner); 147 | 148 | console.log('Block number: ', await provider.getBlockNumber()); 149 | 150 | expect(balanceAfter).gt(balanceBefore); 151 | console.log( 152 | 'Profit: ', 153 | ethers.utils.formatEther(balanceAfter.sub(balanceBefore)) 154 | ); 155 | 156 | expect(minerBalanceAfter).gt( 157 | minerBalanceBefore.add(ethers.utils.parseEther('2')) 158 | ); 159 | const minerProfit = minerBalanceAfter 160 | .sub(minerBalanceBefore) 161 | .sub(ethers.utils.parseEther('2')); 162 | console.log('Miner profit: ', ethers.utils.formatEther(minerProfit)); 163 | const avgGasPrice = minerProfit.div(gasUsed); 164 | console.log( 165 | 'Avg gas price in gwei: ', 166 | ethers.utils.formatUnits(avgGasPrice, 'gwei') 167 | ); 168 | }); 169 | 170 | it('Can withdraw tokens', async () => { 171 | await snxLiquidator.withdraw(chiAddr); 172 | 173 | const balance = await chi.balanceOf(signer.address); 174 | console.log('Withdraw CHI: ', balance.toString()); 175 | 176 | expect(balance).gt('0'); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /contracts/SnxLiquidator.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import '@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol'; 5 | import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol'; 6 | 7 | import './DydxHelper.sol'; 8 | 9 | import 'hardhat/console.sol'; 10 | 11 | // Standard ERC-20 interface 12 | interface IERC20 { 13 | function totalSupply() external view returns (uint256); 14 | 15 | function balanceOf(address account) external view returns (uint256); 16 | 17 | function transfer(address recipient, uint256 amount) external returns (bool); 18 | 19 | function allowance(address owner, address spender) external view returns (uint256); 20 | 21 | function approve(address spender, uint256 amount) external returns (bool); 22 | 23 | function transferFrom( 24 | address sender, 25 | address recipient, 26 | uint256 amount 27 | ) external returns (bool); 28 | 29 | event Transfer(address indexed from, address indexed to, uint256 value); 30 | event Approval(address indexed owner, address indexed spender, uint256 value); 31 | } 32 | 33 | interface IWETH is IERC20 { 34 | function deposit() external payable; 35 | 36 | function withdraw(uint256 wad) external; 37 | } 38 | 39 | interface ICurveFi { 40 | function exchange( 41 | int128 i, 42 | int128 j, 43 | uint256 dx, 44 | uint256 min_dy 45 | ) external payable returns (uint256); 46 | } 47 | 48 | interface ICurveFiV2 { 49 | function exchange( 50 | int128 i, 51 | int128 j, 52 | uint256 dx, 53 | uint256 min_dy 54 | ) external; 55 | } 56 | 57 | interface IEtherCollatoral { 58 | function liquidateUnclosedLoan(address _loanCreatorsAddress, uint256 _loanID) external; 59 | 60 | function accruedInterestOnLoan(uint256 _loanAmount, uint256 _seconds) 61 | external 62 | view 63 | returns (uint256 interestAmount); 64 | 65 | function getLoan(address _account, uint256 _loanID) 66 | external 67 | view 68 | returns ( 69 | address account, 70 | uint256 collateralAmount, 71 | uint256 loanAmount, 72 | uint256 timeCreated, 73 | uint256 loanID, 74 | uint256 timeClosed, 75 | uint256 accruedInterest, 76 | uint256 totalFees 77 | ); 78 | } 79 | 80 | interface ChiToken { 81 | function freeUpTo(uint256 value) external returns (uint256); 82 | function mint(uint256 value) external; 83 | } 84 | 85 | enum LoanType { 86 | Susd, 87 | Seth 88 | } 89 | 90 | contract SnxLiquidator is IDydxCallee { 91 | address public owner; 92 | IWETH private weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 93 | ISoloMargin private soloMargin = ISoloMargin(0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e); 94 | 95 | address private susdLoan = 0xfED77055B40d63DCf17ab250FFD6948FBFF57B82; 96 | address private sethLoan = 0x7133afF303539b0A4F60Ab9bd9656598BF49E272; 97 | 98 | IERC20 private immutable susd = IERC20(0x57Ab1ec28D129707052df4dF418D58a2D46d5f51); 99 | IERC20 private immutable seth = IERC20(0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb); 100 | 101 | address private immutable usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 102 | 103 | IQuoter private immutable quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); 104 | ISwapRouter private immutable uniRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); 105 | 106 | ICurveFiV2 private immutable curveSusdPool = ICurveFiV2(0xA5407eAE9Ba41422680e2e00537571bcC53efBfD); 107 | ICurveFi private immutable curveSethPool = ICurveFi(0xc5424B857f758E906013F3555Dad202e4bdB4567); 108 | 109 | ChiToken private constant chi = ChiToken(0x0000000000004946c0e9F43F4Dee607b0eF1fA1c); 110 | 111 | modifier discountCHI { 112 | uint256 gasStart = gasleft(); 113 | 114 | _; 115 | 116 | uint256 gasSpent = 21000 + gasStart - gasleft() + 16 * msg.data.length; 117 | console.log('Gas spent: ', gasSpent); 118 | chi.freeUpTo((gasSpent + 14154) / 41947); 119 | } 120 | 121 | constructor() { 122 | owner = msg.sender; 123 | weth.approve(address(soloMargin), type(uint256).max); 124 | } 125 | 126 | receive() external payable {} 127 | 128 | function mintCHI(uint256 value) external { 129 | chi.mint(value); 130 | } 131 | 132 | function withdraw(address token) external { 133 | require(owner == msg.sender, 'NO'); 134 | if (token == address(0)) { 135 | payable(msg.sender).transfer(address(this).balance); 136 | } else { 137 | IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); 138 | } 139 | } 140 | 141 | function liquidate( 142 | address account, 143 | uint256 loanID, 144 | LoanType loanType, 145 | uint256 bribeBP 146 | ) external discountCHI { 147 | IEtherCollatoral loanContract; 148 | if (loanType == LoanType.Susd) { 149 | loanContract = IEtherCollatoral(susdLoan); 150 | } else { 151 | loanContract = IEtherCollatoral(sethLoan); 152 | } 153 | 154 | (, , uint256 loanAmount, , , uint256 timeClosed, uint256 accruedInterest, ) = loanContract.getLoan( 155 | account, 156 | loanID 157 | ); 158 | 159 | uint256 repayAmount = loanAmount + accruedInterest; 160 | console.log('SUSD/SETH amount waiting to repay: ', repayAmount / 1e18); 161 | 162 | require(timeClosed == 0, 'Closed'); 163 | require(repayAmount > 0, 'Zero Repay'); 164 | 165 | if (loanType == LoanType.Susd) { 166 | // Note: susd uses 18 decimals while usdc uses 6 decimals 167 | // and we just roughly estimate the slippage on curve is 2% 168 | uint256 usdcAmount = ((repayAmount / 1e12) * 100) / 98; 169 | // roughly use 200 to avoid more calc since flash loan fee is low in dydx 170 | uint256 wethLoanAmount = 200 ether; 171 | dydxFlashLoan(account, loanID, wethLoanAmount, repayAmount, loanType, usdcAmount); 172 | } else { 173 | // slippage on curve is about 1.5%, use 2% here 174 | uint256 wethLoanAmount = (repayAmount * 100) / 98; 175 | // console.log('WETH loan amount: ', wethLoanAmount / 1e18); 176 | dydxFlashLoan(account, loanID, wethLoanAmount, repayAmount, loanType, 0); 177 | } 178 | 179 | uint256 ethBalance = address(this).balance; 180 | // console.log('ETH balance after repay the debt: ', ethBalance / 1e18); 181 | 182 | // calculate bribe for miner 183 | uint256 bribeAmount = 0; 184 | if (bribeBP > 0) { 185 | bribeAmount = (ethBalance * bribeBP) / 10000; 186 | bribe(bribeAmount); 187 | } 188 | 189 | payable(owner).transfer(ethBalance - bribeAmount); 190 | } 191 | 192 | function bribe(uint256 amount) internal { 193 | // console.log('Bribe amount: ', amount); 194 | block.coinbase.call{value: amount}(''); 195 | } 196 | 197 | function dydxFlashLoan( 198 | address snxAccount, 199 | uint256 snxLoanID, 200 | uint256 loanAmount, 201 | uint256 repayAmount, 202 | LoanType loanType, 203 | uint256 usdcAmount 204 | ) internal { 205 | Actions.ActionArgs[] memory operations = new Actions.ActionArgs[](3); 206 | 207 | operations[0] = Actions.ActionArgs({ 208 | actionType: Actions.ActionType.Withdraw, 209 | accountId: 0, 210 | amount: Types.AssetAmount({ 211 | sign: false, 212 | denomination: Types.AssetDenomination.Wei, 213 | ref: Types.AssetReference.Delta, 214 | value: loanAmount // Amount to borrow 215 | }), 216 | primaryMarketId: 0, // WETH 217 | secondaryMarketId: 0, 218 | otherAddress: address(this), 219 | otherAccountId: 0, 220 | data: '' 221 | }); 222 | 223 | operations[1] = Actions.ActionArgs({ 224 | actionType: Actions.ActionType.Call, 225 | accountId: 0, 226 | amount: Types.AssetAmount({ 227 | sign: false, 228 | denomination: Types.AssetDenomination.Wei, 229 | ref: Types.AssetReference.Delta, 230 | value: 0 231 | }), 232 | primaryMarketId: 0, 233 | secondaryMarketId: 0, 234 | otherAddress: address(this), 235 | otherAccountId: 0, 236 | data: abi.encode(snxAccount, snxLoanID, loanAmount, repayAmount, loanType, usdcAmount) 237 | }); 238 | 239 | operations[2] = Actions.ActionArgs({ 240 | actionType: Actions.ActionType.Deposit, 241 | accountId: 0, 242 | amount: Types.AssetAmount({ 243 | sign: true, 244 | denomination: Types.AssetDenomination.Wei, 245 | ref: Types.AssetReference.Delta, 246 | value: loanAmount + 2 // Repayment amount with 2 wei fee 247 | }), 248 | primaryMarketId: 0, // WETH 249 | secondaryMarketId: 0, 250 | otherAddress: address(this), 251 | otherAccountId: 0, 252 | data: '' 253 | }); 254 | 255 | Account.Info[] memory accountInfos = new Account.Info[](1); 256 | accountInfos[0] = Account.Info({owner: address(this), number: 1}); 257 | 258 | soloMargin.operate(accountInfos, operations); 259 | } 260 | 261 | // Dydx flash loan callback function 262 | function callFunction( 263 | address sender, 264 | Account.Info memory, 265 | bytes memory data 266 | ) external override { 267 | require(sender == address(this), 'Not from this contract'); 268 | 269 | ( 270 | address account, 271 | uint256 loanID, 272 | uint256 loanAmount, 273 | uint256 repayAmount, 274 | LoanType loanType, 275 | uint256 usdcAmount 276 | ) = abi.decode(data, (address, uint256, uint256, uint256, LoanType, uint256)); 277 | 278 | if (loanType == LoanType.Susd) { 279 | weth.approve(address(uniRouter), loanAmount); 280 | ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams( 281 | address(weth), 282 | usdc, 283 | 3000, 284 | address(this), 285 | block.timestamp, 286 | usdcAmount, 287 | loanAmount, 288 | 0 289 | ); 290 | uint256 wethSpent = uniRouter.exactOutputSingle(params); 291 | 292 | // coin index in curve susd pool: 1 => usdc, 3 => susd 293 | // swap usdc to susd 294 | IERC20(usdc).approve(address(curveSusdPool), usdcAmount); 295 | curveSusdPool.exchange(1, 3, usdcAmount, repayAmount); 296 | 297 | susd.approve(susdLoan, repayAmount); 298 | IEtherCollatoral(susdLoan).liquidateUnclosedLoan(account, loanID); 299 | 300 | weth.deposit{value: wethSpent + 2}(); 301 | } else { 302 | weth.withdraw(loanAmount); 303 | // coin index in curve seth pool: 0 => eth, 1 => seth 304 | // swap eth for seth 305 | curveSethPool.exchange{value: loanAmount}(0, 1, loanAmount, repayAmount); 306 | 307 | seth.approve(sethLoan, repayAmount); 308 | IEtherCollatoral(sethLoan).liquidateUnclosedLoan(account, loanID); 309 | 310 | weth.deposit{value: loanAmount + 2}(); 311 | } 312 | } 313 | } 314 | --------------------------------------------------------------------------------