2021-03-09 05:32:07 -08:00
|
|
|
import {
|
2021-03-09 05:44:39 -08:00
|
|
|
findLargestTokenAccountForOwner,
|
2021-03-09 05:32:07 -08:00
|
|
|
getMultipleAccounts,
|
|
|
|
IDS,
|
|
|
|
MangoClient,
|
|
|
|
MangoGroup,
|
|
|
|
MarginAccount,
|
2021-03-09 05:44:39 -08:00
|
|
|
nativeToUi,
|
|
|
|
NUM_MARKETS,
|
2021-04-15 07:24:29 -07:00
|
|
|
NUM_TOKENS, parseTokenAccount,
|
2021-04-06 17:43:51 -07:00
|
|
|
parseTokenAccountData, tokenToDecimals, uiToNative,
|
2021-03-09 05:44:39 -08:00
|
|
|
} from '@blockworks-foundation/mango-client';
|
2021-04-06 17:43:51 -07:00
|
|
|
import { Account, Connection, PublicKey, Transaction, TransactionSignature } from '@solana/web3.js';
|
2021-03-09 05:32:07 -08:00
|
|
|
import fs from 'fs';
|
2021-03-09 05:44:39 -08:00
|
|
|
import { Market } from '@project-serum/serum';
|
2021-03-13 15:24:23 -08:00
|
|
|
import { notify, sleep } from './utils';
|
2021-03-09 05:44:39 -08:00
|
|
|
import { homedir } from 'os';
|
2021-04-06 17:43:51 -07:00
|
|
|
import {
|
|
|
|
makeForceCancelOrdersInstruction,
|
|
|
|
makePartialLiquidateInstruction,
|
|
|
|
} from '@blockworks-foundation/mango-client/lib/instruction';
|
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
|
|
|
|
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')
|
|
|
|
|
2021-03-14 10:45:58 -07:00
|
|
|
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
|
2021-03-09 05:32:07 -08:00
|
|
|
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]])
|
|
|
|
}
|
2021-03-14 10:45:58 -07:00
|
|
|
|
|
|
|
// Sort by those with largest net deposits and sell those first before trying to buy back the borrowed
|
2021-03-09 05:32:07 -08:00
|
|
|
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]
|
2021-06-11 12:53:17 -07:00
|
|
|
const tokenDecimals = mangoGroup.getTokenDecimals(marketIndex)
|
2021-03-14 10:45:58 -07:00
|
|
|
const tokenDecimalAdj = Math.pow(10, tokenDecimals)
|
2021-03-09 05:32:07 -08:00
|
|
|
|
|
|
|
if (netValues[i][1] > 0) { // sell to close
|
|
|
|
const price = prices[marketIndex] * 0.95
|
2021-03-14 10:45:58 -07:00
|
|
|
const size = Math.floor(assets[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj // round down the size
|
|
|
|
if (size === 0) {
|
|
|
|
continue
|
|
|
|
}
|
2021-03-09 05:32:07 -08:00
|
|
|
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
|
2021-03-14 10:45:58 -07:00
|
|
|
const size = Math.ceil(liabs[marketIndex] * tokenDecimalAdj) / tokenDecimalAdj
|
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
console.log(`Buy to close ${marketIndex} ${size}`)
|
|
|
|
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'buy', price, size, 'limit')
|
|
|
|
}
|
|
|
|
}
|
2021-04-06 17:43:51 -07:00
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
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')
|
|
|
|
|
2021-03-14 10:45:58 -07:00
|
|
|
await client.withdraw(connection, programId, mangoGroup, ma, payer, mangoGroup.tokens[NUM_TOKENS-1], usdWallet, ma.getUiDeposit(mangoGroup, NUM_TOKENS-1) * 0.999)
|
2021-03-09 05:32:07 -08:00
|
|
|
console.log('Successfully drained account', ma.publicKey.toString())
|
|
|
|
}
|
|
|
|
|
|
|
|
async function runLiquidator() {
|
|
|
|
const client = new MangoClient()
|
|
|
|
const cluster = process.env.CLUSTER || 'mainnet-beta'
|
2021-06-11 12:53:17 -07:00
|
|
|
const group_name = process.env.GROUP_NAME || 'BTC_ETH_SOL_SRM_USDT'
|
2021-03-09 05:32:07 -08:00
|
|
|
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')))
|
|
|
|
|
2021-03-13 15:24:23 -08:00
|
|
|
notify(`liquidator launched cluster=${cluster} group=${group_name}`);
|
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
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)
|
2021-06-11 12:53:17 -07:00
|
|
|
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([
|
2021-04-15 07:24:29 -07:00
|
|
|
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(
|
2021-03-09 05:32:07 -08:00
|
|
|
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
|
|
|
|
)
|
2021-04-15 07:24:29 -07:00
|
|
|
|
|
|
|
console.log(prices)
|
2021-03-09 05:32:07 -08:00
|
|
|
console.log(vaultValues)
|
|
|
|
|
2021-03-26 05:50:58 -07:00
|
|
|
let maxBorrAcc = ""
|
|
|
|
let maxBorrVal = 0;
|
2021-03-09 05:32:07 -08:00
|
|
|
for (let ma of marginAccounts) { // parallelize this if possible
|
|
|
|
|
|
|
|
let liquidated = false
|
2021-03-16 04:35:07 -07:00
|
|
|
let description = ''
|
2021-04-14 14:41:15 -07:00
|
|
|
try {
|
|
|
|
const assetsVal = ma.getAssetsVal(mangoGroup, prices)
|
|
|
|
const liabsVal = ma.getLiabsVal(mangoGroup, prices)
|
|
|
|
if (liabsVal > maxBorrVal) {
|
|
|
|
maxBorrVal = liabsVal
|
|
|
|
maxBorrAcc = ma.publicKey.toBase58()
|
|
|
|
}
|
2021-03-09 05:32:07 -08:00
|
|
|
|
2021-04-14 14:41:15 -07:00
|
|
|
if (liabsVal < 0.1) { // too small of an account; number precision may cause errors
|
2021-04-14 14:48:32 -07:00
|
|
|
continue
|
2021-04-14 14:41:15 -07:00
|
|
|
}
|
|
|
|
let collRatio = (assetsVal / liabsVal)
|
2021-03-09 05:32:07 -08:00
|
|
|
|
2021-04-14 14:41:15 -07:00
|
|
|
// 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) {
|
2021-04-14 14:48:32 -07:00
|
|
|
continue
|
2021-04-14 14:41:15 -07:00
|
|
|
}
|
2021-03-19 11:44:15 -07:00
|
|
|
|
2021-04-14 14:41:15 -07:00
|
|
|
const deficit = liabsVal * mangoGroup.initCollRatio - assetsVal
|
|
|
|
description = ma.toPrettyString(mangoGroup, prices)
|
2021-03-19 11:44:15 -07:00
|
|
|
|
2021-04-14 14:41:15 -07:00
|
|
|
if (deficit < 0.1) { // too small of an account; number precision may cause errors
|
2021-04-14 14:48:32 -07:00
|
|
|
continue
|
2021-04-14 14:41:15 -07:00
|
|
|
}
|
2021-04-15 07:39:09 -07:00
|
|
|
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
|
|
|
|
}
|
2021-04-14 14:41:15 -07:00
|
|
|
console.log('liquidatable', deficit)
|
|
|
|
console.log(description)
|
|
|
|
await client.liquidate(connection, programId, mangoGroup, ma, payer,
|
2021-04-15 07:24:29 -07:00
|
|
|
tokenWallets, [0, 0, depositAmount])
|
2021-04-14 14:41:15 -07:00
|
|
|
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)
|
2021-03-09 05:32:07 -08:00
|
|
|
}
|
|
|
|
}
|
2021-04-14 14:41:15 -07:00
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
if (liquidated) {
|
|
|
|
console.log('liquidation success')
|
|
|
|
console.log(ma.toPrettyString(mangoGroup, prices))
|
|
|
|
|
2021-04-14 14:41:15 -07:00
|
|
|
let tries = 3
|
|
|
|
while (tries > 0) {
|
2021-03-09 05:32:07 -08:00
|
|
|
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')
|
2021-03-16 04:35:07 -07:00
|
|
|
notify(`liquidated ${description}`)
|
2021-03-09 05:32:07 -08:00
|
|
|
break
|
|
|
|
} catch (e) {
|
2021-04-14 14:41:15 -07:00
|
|
|
tries -= 1
|
|
|
|
notify(`error: ${e}\ncould not drain account ${description}\ntries left: ${tries}`)
|
2021-03-09 05:32:07 -08:00
|
|
|
await sleep(1000)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-26 05:50:58 -07:00
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
}
|
|
|
|
|
2021-03-26 05:50:58 -07:00
|
|
|
console.log(`Max Borrow Account: ${maxBorrAcc} | Max Borrow Val: ${maxBorrVal}`)
|
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
} catch (e) {
|
2021-03-13 15:24:23 -08:00
|
|
|
notify(`unknown error: ${e}`);
|
|
|
|
console.error(e);
|
2021-03-09 05:32:07 -08:00
|
|
|
} finally {
|
|
|
|
await sleep(sleepTime)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-06 17:43:51 -07:00
|
|
|
|
2021-03-09 05:32:07 -08:00
|
|
|
runLiquidator()
|
2021-04-06 17:43:51 -07:00
|
|
|
// runPartialLiquidator()
|