liquidator/src/liquidate.ts

259 lines
9.5 KiB
TypeScript

import * as Env from 'dotenv';
Env.config();
import {
findLargestTokenAccountForOwner,
getMultipleAccounts,
IDS,
MangoClient,
MangoGroup,
MarginAccount,
nativeToUi,
NUM_MARKETS,
NUM_TOKENS, parseTokenAccount,
parseTokenAccountData, tokenToDecimals, uiToNative,
} from '@blockworks-foundation/mango-client';
import { Account, Connection, PublicKey, Transaction, TransactionSignature } from '@solana/web3.js';
import fs from 'fs';
import { Market } from '@project-serum/serum';
import { notify, sleep } from './utils';
import { homedir } from 'os';
import {
makeForceCancelOrdersInstruction,
makePartialLiquidateInstruction,
} from '@blockworks-foundation/mango-client/lib/instruction';
async function drainAccount(
client: MangoClient,
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
ma: MarginAccount,
markets: Market[],
payer: Account,
prices: number[],
usdWallet: PublicKey
) {
// Cancel all open orders
const bidsPromises = markets.map((market) => market.loadBids(connection))
const asksPromises = markets.map((market) => market.loadAsks(connection))
const books = await Promise.all(bidsPromises.concat(asksPromises))
const bids = books.slice(0, books.length / 2)
const asks = books.slice(books.length / 2, books.length)
const cancelProms: Promise<TransactionSignature[]>[] = []
for (let i = 0; i < NUM_MARKETS; i++) {
cancelProms.push(ma.cancelAllOrdersByMarket(connection, client, programId, mangoGroup, markets[i], bids[i], asks[i], payer))
}
await Promise.all(cancelProms)
console.log('all orders cancelled')
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
console.log('settleAll complete')
await sleep(2000)
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
// sort non-quote currency assets by value
const assets = ma.getAssets(mangoGroup)
const liabs = ma.getLiabs(mangoGroup)
const netValues: [number, number][] = []
for (let i = 0; i < NUM_TOKENS - 1; i++) {
netValues.push([i, (assets[i] - liabs[i]) * prices[i]])
}
// Sort by those with largest net deposits and sell those first before trying to buy back the borrowed
netValues.sort((a, b) => (b[1] - a[1]))
for (let i = 0; i < NUM_TOKENS - 1; i++) {
const marketIndex = netValues[i][0]
const market = markets[marketIndex]
const tokenDecimals = mangoGroup.getTokenDecimals(marketIndex)
const tokenDecimalAdj = Math.pow(10, tokenDecimals)
if (netValues[i][1] > 0) { // sell to close
const price = prices[marketIndex] * 0.95
const size = Math.floor(assets[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj // round down the size
if (size === 0) {
continue
}
console.log(`Sell to close ${marketIndex} ${size}`)
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'sell', price, size, 'limit')
} else if (netValues[i][1] < 0) { // buy to close
const price = prices[marketIndex] * 1.05 // buy at up to 5% higher than oracle price
const size = Math.ceil(liabs[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj
console.log(`Buy to close ${marketIndex} ${size}`)
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'buy', price, size, 'limit')
}
}
await sleep(3000)
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
console.log('settleAll complete')
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
console.log('Liquidation process complete\n', ma.toPrettyString(mangoGroup, prices))
console.log('Withdrawing USD')
await client.withdraw(connection, programId, mangoGroup, ma, payer, mangoGroup.tokens[NUM_TOKENS-1], usdWallet, ma.getUiDeposit(mangoGroup, NUM_TOKENS-1) * 0.999)
console.log('Successfully drained account', ma.publicKey.toString())
}
async function runLiquidator() {
const client = new MangoClient()
const cluster = process.env.CLUSTER || 'mainnet-beta'
const group_name = process.env.GROUP_NAME || 'BTC_ETH_SOL_SRM_USDT'
const clusterUrl = process.env.CLUSTER_URL || IDS.cluster_urls[cluster]
const connection = new Connection(clusterUrl, 'singleGossip')
// The address of the Mango Program on the blockchain
const programId = new PublicKey(IDS[cluster].mango_program_id)
// The address of the serum dex program on the blockchain: https://github.com/project-serum/serum-dex
const dexProgramId = new PublicKey(IDS[cluster].dex_program_id)
// Address of the MangoGroup
const mangoGroupPk = new PublicKey(IDS[cluster].mango_groups[group_name].mango_group_pk)
// liquidator's keypair
const keyPairPath = process.env.KEYPAIR || homedir() + '/.config/solana/id.json'
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
notify(`liquidator launched cluster=${cluster} group=${group_name}`);
let mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
const tokenWallets = (await Promise.all(
mangoGroup.tokens.map(
(mint) => findLargestTokenAccountForOwner(connection, payer.publicKey, mint).then(
(response) => response.publicKey
)
)
))
// load all markets
const markets = await Promise.all(mangoGroup.spotMarkets.map(
(pk) => Market.load(connection, pk, {skipPreflight: true, commitment: 'singleGossip'}, dexProgramId)
))
const sleepTime = 5000
// TODO handle failures in any of the steps
// Find a way to get all margin accounts without querying fresh--get incremental updates to margin accounts
while (true) {
try {
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
let marginAccounts = process.env.FILTER_ACCOUNTS ?
await client.getAllMarginAccountsWithBorrows(connection, programId, mangoGroup) :
await client.getAllMarginAccounts(connection, programId, mangoGroup)
let [prices, vaultAccs, liqorAccs] = await Promise.all([
mangoGroup.getPrices(connection),
getMultipleAccounts(connection, mangoGroup.vaults),
getMultipleAccounts(connection, tokenWallets),
])
const vaultValues = vaultAccs.map(
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
const liqorTokenUi = liqorAccs.map(
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
)
console.log(prices)
console.log(vaultValues)
let maxBorrAcc = ""
let maxBorrVal = 0;
for (let ma of marginAccounts) { // parallelize this if possible
let liquidated = false
let description = ''
try {
const assetsVal = ma.getAssetsVal(mangoGroup, prices)
const liabsVal = ma.getLiabsVal(mangoGroup, prices)
if (liabsVal > maxBorrVal) {
maxBorrVal = liabsVal
maxBorrAcc = ma.publicKey.toBase58()
}
if (liabsVal < 0.1) { // too small of an account; number precision may cause errors
continue
}
let collRatio = (assetsVal / liabsVal)
// FIXME: added bias to collRatio to allow other liquidators to step in for testing
if (process.env.COLL_BIAS) {
collRatio += parseFloat(process.env.COLL_BIAS);
}
if (collRatio >= mangoGroup.maintCollRatio) {
continue
}
const deficit = liabsVal * mangoGroup.initCollRatio - assetsVal
description = ma.toPrettyString(mangoGroup, prices)
if (deficit < 0.1) { // too small of an account; number precision may cause errors
continue
}
const depositAmount = deficit * 1.01 + 5
if (liqorTokenUi[NUM_TOKENS-1] < depositAmount) {
console.log(`Liquidator does not have enough funds. ${liqorTokenUi[NUM_TOKENS-1]} < ${depositAmount}`)
continue
}
console.log('liquidatable', deficit)
console.log(description)
await client.liquidate(connection, programId, mangoGroup, ma, payer,
tokenWallets, [0, 0, depositAmount])
liquidated = true
} catch (e) {
if (!e.timeout) {
throw e
} else {
await sleep(1000)
prices = await mangoGroup.getPrices(connection)
ma = await client.getMarginAccount(connection, ma.publicKey, dexProgramId)
}
}
if (liquidated) {
console.log('liquidation success')
console.log(ma.toPrettyString(mangoGroup, prices))
let tries = 3
while (tries > 0) {
try {
ma = await client.getMarginAccount(connection, ma.publicKey, dexProgramId)
await drainAccount(client, connection, programId, mangoGroup, ma, markets, payer, prices, tokenWallets[NUM_TOKENS-1])
console.log('Account drain success')
notify(`liquidated ${description}`)
break
} catch (e) {
tries -= 1
notify(`error: ${e}\ncould not drain account ${description}\ntries left: ${tries}`)
await sleep(1000)
}
}
}
}
console.log(`Max Borrow Account: ${maxBorrAcc} | Max Borrow Val: ${maxBorrVal}`)
} catch (e) {
notify(`unknown error: ${e}`);
console.error(e);
} finally {
await sleep(sleepTime)
}
}
}
runLiquidator()
// runPartialLiquidator()