From 630c170e7b44d7977fda56331426ee91400e2c23 Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 8 Jan 2024 21:15:45 -0500 Subject: [PATCH 1/3] wip liquidator --- ts/client/scripts/liquidator/index.ts | 396 ++++++++++++++++++++++++++ ts/client/src/client.ts | 168 +++++++++++ 2 files changed, 564 insertions(+) create mode 100644 ts/client/scripts/liquidator/index.ts diff --git a/ts/client/scripts/liquidator/index.ts b/ts/client/scripts/liquidator/index.ts new file mode 100644 index 000000000..38e4eb9fd --- /dev/null +++ b/ts/client/scripts/liquidator/index.ts @@ -0,0 +1,396 @@ +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; +import fs from 'fs'; +import { MangoClient } from '../../src/client'; +import { MANGO_V4_ID } from '../../src/constants'; +import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../../src/numbers/I80F48'; +import { MangoAccount } from '../../src/accounts/mangoAccount'; +import { Group } from '../../src/accounts/group'; +import { PerpMarketIndex, PerpOrderSide } from '../../src/accounts/perp'; +import { MarketIndex } from '../../src/accounts/serum3'; +import { HealthCache } from '../../src/accounts/healthCache'; +import { MangoSignatureStatus } from '../../src/utils/rpc'; +import { TokenIndex } from '../../src/accounts/bank'; + +// +// This script creates liquidation candidates +// + +const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'); +const CLUSTER = process.env.CLUSTER || 'mainnet-beta'; +const RPC_URL = process.env.RPC_URL; +const WALLET_KEYPAIR = process.env.KEYPAIR; +const LIQUIDATOR_MANGO_ACCT_NUM = process.env.ACCT_NUM || '0'; +const LIAB_LIMIT = I80F48.fromNumber( + Math.min(parseFloat(process.env.LIAB_LIMIT || '0.9'), 1), +); + +const main = async (): Promise => { + const options = AnchorProvider.defaultOptions(); + options.commitment = 'processed'; + options.preflightCommitment = 'finalized'; + const connection = new Connection(RPC_URL!, options); + + const keypair = Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(WALLET_KEYPAIR!, 'utf-8'))), + ); + const userWallet = new Wallet(keypair); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = MangoClient.connect( + userProvider, + CLUSTER as Cluster, + MANGO_V4_ID[CLUSTER], + { + idsSource: 'get-program-accounts', + prioritizationFee: 100, + txConfirmationCommitment: 'confirmed', + }, + ); + + // fetch group + const group = await client.getGroup(GROUP); + // liquidator's mango account + const liquidatorMangoAccount = await client.getMangoAccountForOwner( + group, + keypair.publicKey, + parseFloat(LIQUIDATOR_MANGO_ACCT_NUM), + ); + if (!liquidatorMangoAccount) { + throw new Error('liquidatorMangoAccount not found'); + } + const mangoAccounts = await client.getAllMangoAccounts(group, true); + + // loop over all mangoAccounts and find liquidable ones + for (const mangoAccount of mangoAccounts) { + if (!isLiquidable(mangoAccount, group)) { + continue; + } + + console.log(`Attempting to liquidate ${mangoAccount.publicKey.toBase58()}`); + + try { + liquidateAccount(mangoAccount, liquidatorMangoAccount, group, client); + } catch (e) { + console.error( + `Error liquidating ${mangoAccount.publicKey.toBase58()}, ${e}`, + ); + } + } +}; + +main(); + +function isLiquidable(mangoAccount: MangoAccount, group: Group): boolean { + return ( + mangoAccount.getHealth(group, 'Init').isNeg() || + mangoAccount.getHealth(group, 'Maint').isNeg() + ); +} + +async function liquidateAccount( + mangoAccount: MangoAccount, + liquidatorMangoAccount: MangoAccount, + group: Group, + client: MangoClient, +): Promise { + // Phase 1? + try { + // cancel all perp open orders + await Promise.all( + mangoAccount.perpOpenOrders.map((perpOo) => { + return client.perpForceClosePosition( + group, + perpOo.orderMarket as PerpMarketIndex, + mangoAccount, + liquidatorMangoAccount, + ); + }), + ); + } catch (e) { + console.error(`Error cancelling perp orders: ${e}`); + } + + await mangoAccount.reload(client); + if (!isLiquidable(mangoAccount, group)) { + throw new Error('Account is no longer liquidable'); + } + + try { + // cancel all serum open orders + await Promise.all( + Object.entries(mangoAccount.serum3OosMapByMarketIndex).map(([mktIdx]) => { + return client.serum3LiqForceCancelOrders( + group, + mangoAccount, + group.getSerum3MarketByMarketIndex(parseFloat(mktIdx) as MarketIndex) + .publicKey, + ); + }), + ); + } catch (e) { + console.error(`Error cancelling sersum open orders: ${e}`); + } + + await mangoAccount.reload(client); + if (!isLiquidable(mangoAccount, group)) { + throw new Error('Account is no longer liquidable'); + } + + const liqorHealthCache = HealthCache.fromMangoAccount( + group, + liquidatorMangoAccount, + ); + + const liqeeHealthCache = HealthCache.fromMangoAccount( + group, + liquidatorMangoAccount, + ); + + // Phase 2? + // TODO: should we return if this succeeds? + await liquidatePerpsBaseOrPosPnl( + mangoAccount, + liquidatorMangoAccount, + liqorHealthCache, + liqeeHealthCache, + group, + client, + ); + + // TODO: should we return if this succeeds? + await liquidateToken( + mangoAccount, + liquidatorMangoAccount, + liqorHealthCache, + liqeeHealthCache, + group, + client, + ); + + // Phase 3? + // TODO: should we return if this succeeds? + await liquidatePerpsNegPnl( + mangoAccount, + liquidatorMangoAccount, + group, + client, + ); + + // TODO: should we return if this succeeds? + await liquidateTokenBankruptcy( + mangoAccount, + liquidatorMangoAccount, + liqorHealthCache, + group, + client, + ); +} + +async function liquidateTokenBankruptcy( + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + liqorHealthCache: HealthCache, + group: Group, + client: MangoClient, +): Promise { + const tokens = liqeeMangoAccount + .tokensActive() + .map((t) => { + const bank = group.getFirstBankByTokenIndex(t.tokenIndex); + const price = bank._price; + if (!price) { + throw new Error('price not found in liquidateTokenBankruptcy'); + } + const liabUsdcEquivalent = t.balance(bank).mul(price); + + return { + ...t, + price, + liabUsdcEquivalent, + }; + }) + .sort((a, b) => b.liabUsdcEquivalent.sub(a.liabUsdcEquivalent).toNumber()); + + const tokenLiab = tokens[0]; + + const assetBank = group.getFirstBankByTokenIndex(0 as TokenIndex); // USDC + const liabBank = group.getFirstBankByTokenIndex( + tokenLiab.tokenIndex as TokenIndex, + ); + if (!assetBank?._price || !liabBank?._price) { + throw new Error('asset price or liab bank price not found'); + } + // TODO: check if this is correct + const price = assetBank._price.div(liabBank._price); + + const maxLiabTransfer = liqorHealthCache.getMaxSwapSourceForHealthRatio( + liabBank, + assetBank, + price, + LIAB_LIMIT, // TODO: is this correct? what is a good default for this? + ); + + await client.tokenLiqBankruptcy( + group, + liqeeMangoAccount, + liqorMangoAccount, + assetBank.mint, + liabBank.mint, + maxLiabTransfer, + ); +} + +async function liquidateToken( + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + liqorHealthCache: HealthCache, + liqeeHealthCache: HealthCache, + group: Group, + client: MangoClient, +): Promise { + let minNet = ZERO_I80F48(); + let minNetIndex = -1; + let maxNet = ZERO_I80F48(); + let maxNetIndex = -1; + + for (const [i, token] of liqeeMangoAccount.tokensActive().entries()) { + const bank = group.getFirstBankByTokenIndex(token.tokenIndex); + const price = bank._price; + if (!price) { + throw new Error('price not found'); + } + const netDeposit = token.deposits(bank).sub(token.borrows(bank)).mul(price); + + if (netDeposit.lt(minNet)) { + minNet = netDeposit; + minNetIndex = i; + } else if (netDeposit.gt(maxNet)) { + maxNet = netDeposit; + maxNetIndex = i; + } + } + + if (minNetIndex == -1) { + throw new Error('min net index neg 1'); + } + + const assetBank = group.getFirstBankByTokenIndex(maxNetIndex as TokenIndex); + const liabBank = group.getFirstBankByTokenIndex(minNetIndex as TokenIndex); + if (!assetBank?._price || !liabBank?._price) { + throw new Error('asset price or liab bank price not found'); + } + // TODO: check if this is correct + const price = assetBank._price.div(liabBank._price); + + const maxLiabTransfer = liqorHealthCache.getMaxSwapSourceForHealthRatio( + liabBank, + assetBank, + price, + LIAB_LIMIT, // TODO: is this correct? what is a good default for this? + ); + + await client.liqTokenWithToken( + group, + liqorMangoAccount, + liqeeMangoAccount, + assetBank.publicKey, + liabBank.publicKey, + maxLiabTransfer.toNumber(), + ); +} + +async function liquidatePerpsBaseOrPosPnl( + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + liqorHealthCache: HealthCache, + liqeeHealthCache: HealthCache, + group: Group, + client: MangoClient, +): Promise { + const sortedPerpPositions = liqeeMangoAccount + .perpActive() + .map((pp) => { + const perpMarket = group.getPerpMarketByMarketIndex(pp.marketIndex); + const basePos = pp.getBasePositionUi(perpMarket); + const quotePos = pp.getQuotePositionUi(perpMarket); + const baseVal = basePos * perpMarket._uiPrice; + + return { + ...pp, + marketIndex: pp.marketIndex, + perpMarket, + basePos, + quotePos, + baseVal, + quoteVal: quotePos, + side: pp.getBasePosition(perpMarket).isNeg() + ? PerpOrderSide.ask + : PerpOrderSide.bid, + }; + }) + .sort((a, b) => { + return b.baseVal - a.baseVal; + }); + const highestValuePerpPosition = sortedPerpPositions[0]; + + const maxBaseTransfer = liqorHealthCache.getMaxPerpForHealthRatio( + highestValuePerpPosition.perpMarket, + highestValuePerpPosition.perpMarket._price, + highestValuePerpPosition.side, + LIAB_LIMIT, // TODO: is this correct? what is a good default for this? + ); + + const maxPerpUnsettledLeverage = I80F48.fromNumber(0.95); + const perpUnsettledCost = ONE_I80F48() + .sub(highestValuePerpPosition.perpMarket.initOverallAssetWeight) + .min(maxPerpUnsettledLeverage); + + const maxUsdcBorrow = liqorMangoAccount.getMaxWithdrawWithBorrowForToken( + group, + group.getFirstBankForPerpSettlement().mint, + ); + const allowedUsdcBorrow = I80F48.fromNumber(0.25).mul(maxUsdcBorrow); + const maxPnlTransfer = allowedUsdcBorrow.div(perpUnsettledCost); + + return await client.perpLiqBaseOrPositivePnl( + group, + liqeeMangoAccount, + liqorMangoAccount, + highestValuePerpPosition.perpMarket.perpMarketIndex, + maxBaseTransfer.toNumber(), + maxPnlTransfer.toNumber(), + ); +} + +async function liquidatePerpsNegPnl( + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + group: Group, + client: MangoClient, +): Promise { + const sortedPerpPositions = liqeeMangoAccount + .perpActive() + .map((pp) => { + const perpMarket = group.getPerpMarketByMarketIndex(pp.marketIndex); + const quotePos = pp.getQuotePositionUi(perpMarket); + return { + ...pp, + quotePos, + }; + }) + .filter((pp) => { + return pp.quotePos >= 0 ? false : true; + }) + .sort((a, b) => { + return a.quotePos - b.quotePos; + }); + + const mostNegPerpPosition = sortedPerpPositions[0]; + + return await client.perpLiqNegativePnlOrBankruptcy( + group, + liqeeMangoAccount, + liqorMangoAccount, + mostNegPerpPosition.marketIndex, + ); +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 6ddf6cf01..2793ad26d 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -3619,6 +3619,174 @@ export class MangoClient { /// liquidations + public async perpLiqBaseOrPositivePnlIx( + group: Group, + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + perpMarketIndex: PerpMarketIndex, + maxBaseTransfer: number, + maxPnlTransfer: number, + ): Promise { + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const bank = group.getFirstBankByTokenIndex(perpMarket.settleTokenIndex); + const healthRemainingAccounts: PublicKey[] = + this.buildHealthRemainingAccounts( + group, + [liqeeMangoAccount], + [group.getFirstBankForPerpSettlement()], + [perpMarket], + ); + + return await this.program.methods + .perpLiqBaseOrPositivePnl(new BN(maxBaseTransfer), new BN(maxPnlTransfer)) + .accounts({ + group: group.publicKey, + perpMarket: perpMarket.publicKey, + oracle: perpMarket.oracle, + liqor: liqorMangoAccount.publicKey, + liqorOwner: (this.program.provider as AnchorProvider).wallet.publicKey, + liqee: liqeeMangoAccount.publicKey, + settleBank: bank.publicKey, + settleVault: bank.vault, + settleOracle: bank.oracle, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), + ), + ) + .instruction(); + } + + public async perpLiqNegativePnlOrBankruptcy( + group: Group, + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + perpMarketIndex: PerpMarketIndex, + ): Promise { + return await this.sendAndConfirmTransactionForGroup(group, [ + await this.perpLiqNegativePnlOrBankruptcyIx( + group, + liqeeMangoAccount, + liqorMangoAccount, + perpMarketIndex, + ), + ]); + } + + public async perpLiqNegativePnlOrBankruptcyIx( + group: Group, + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + perpMarketIndex: PerpMarketIndex, + ): Promise { + const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); + const bank = group.getFirstBankByTokenIndex(perpMarket.settleTokenIndex); + const healthRemainingAccounts: PublicKey[] = + this.buildHealthRemainingAccounts( + group, + [liqeeMangoAccount], + [group.getFirstBankForPerpSettlement()], + [perpMarket], + ); + + return await this.program.methods + .perpLiqNegativePnlOrBankruptcy(U64_MAX_BN) + .accounts({ + group: group.publicKey, + perpMarket: perpMarket.publicKey, + oracle: perpMarket.oracle, + liqor: liqorMangoAccount.publicKey, + liqorOwner: (this.program.provider as AnchorProvider).wallet.publicKey, + liqee: liqeeMangoAccount.publicKey, + settleBank: bank.publicKey, + settleOracle: bank.oracle, + settleVault: bank.vault, + insuranceVault: group.insuranceVault, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), + ), + ) + .instruction(); + } + + public async perpLiqBaseOrPositivePnl( + group: Group, + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + perpMarketIndex: PerpMarketIndex, + maxBaseTransfer: number, + maxPnlTransfer: number, + ): Promise { + return await this.sendAndConfirmTransactionForGroup(group, [ + await this.perpLiqBaseOrPositivePnlIx( + group, + liqeeMangoAccount, + liqorMangoAccount, + perpMarketIndex, + maxBaseTransfer, + maxPnlTransfer, + ), + ]); + } + + public async tokenLiqBankruptcyIx( + group: Group, + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + assetMintPk: PublicKey, + liabMintPk: PublicKey, + maxLiabTransfer: I80F48, + ): Promise { + const quoteVault = group.getFirstBankByMint(assetMintPk).vault; + const liabMintInfo = group.mintInfosMapByMint.get(liabMintPk.toString())!; + const healthRemainingAccounts: PublicKey[] = + this.buildHealthRemainingAccounts(group, [liqeeMangoAccount], [], []); + + return await this.program.methods + .tokenLiqBankruptcy({ val: maxLiabTransfer.getData() }) + .accounts({ + group: group.publicKey, + liqor: liqorMangoAccount.publicKey, + liqorOwner: (this.program.provider as AnchorProvider).wallet.publicKey, + liqee: liqeeMangoAccount.publicKey, + liabMintInfo: liabMintInfo.publicKey, + quoteVault: quoteVault, + insuranceVault: group.insuranceVault, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), + ), + ) + .instruction(); + } + + public async tokenLiqBankruptcy( + group: Group, + liqeeMangoAccount: MangoAccount, + liqorMangoAccount: MangoAccount, + assetMintPk: PublicKey, + liabMintPk: PublicKey, + maxLiabTransfer: I80F48, + ): Promise { + return await this.sendAndConfirmTransactionForGroup(group, [ + await this.tokenLiqBankruptcyIx( + group, + liqeeMangoAccount, + liqorMangoAccount, + assetMintPk, + liabMintPk, + maxLiabTransfer, + ), + ]); + } + public async liqTokenWithToken( group: Group, liqor: MangoAccount, From f94267bcf261d816907fdb2479d7382b4d5eb762 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Wed, 10 Jan 2024 09:49:27 +0100 Subject: [PATCH 2/3] --wip-- [skip ci] --- ts/client/scripts/liquidator/index.ts | 34 ++++++++++++--------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/ts/client/scripts/liquidator/index.ts b/ts/client/scripts/liquidator/index.ts index 38e4eb9fd..c335e91db 100644 --- a/ts/client/scripts/liquidator/index.ts +++ b/ts/client/scripts/liquidator/index.ts @@ -1,26 +1,24 @@ import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; -import { MangoClient } from '../../src/client'; -import { MANGO_V4_ID } from '../../src/constants'; -import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../../src/numbers/I80F48'; -import { MangoAccount } from '../../src/accounts/mangoAccount'; +import { TokenIndex } from '../../src/accounts/bank'; import { Group } from '../../src/accounts/group'; +import { HealthCache } from '../../src/accounts/healthCache'; +import { MangoAccount } from '../../src/accounts/mangoAccount'; import { PerpMarketIndex, PerpOrderSide } from '../../src/accounts/perp'; import { MarketIndex } from '../../src/accounts/serum3'; -import { HealthCache } from '../../src/accounts/healthCache'; +import { MangoClient } from '../../src/client'; +import { MANGO_V4_ID } from '../../src/constants'; +import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../../src/numbers/I80F48'; import { MangoSignatureStatus } from '../../src/utils/rpc'; -import { TokenIndex } from '../../src/accounts/bank'; - -// -// This script creates liquidation candidates -// const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'); const CLUSTER = process.env.CLUSTER || 'mainnet-beta'; -const RPC_URL = process.env.RPC_URL; -const WALLET_KEYPAIR = process.env.KEYPAIR; -const LIQUIDATOR_MANGO_ACCT_NUM = process.env.ACCT_NUM || '0'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const USER_KEYPAIR = + process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; const LIAB_LIMIT = I80F48.fromNumber( Math.min(parseFloat(process.env.LIAB_LIMIT || '0.9'), 1), ); @@ -29,10 +27,10 @@ const main = async (): Promise => { const options = AnchorProvider.defaultOptions(); options.commitment = 'processed'; options.preflightCommitment = 'finalized'; - const connection = new Connection(RPC_URL!, options); + const connection = new Connection(CLUSTER_URL!, options); const keypair = Keypair.fromSecretKey( - Buffer.from(JSON.parse(fs.readFileSync(WALLET_KEYPAIR!, 'utf-8'))), + Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))), ); const userWallet = new Wallet(keypair); const userProvider = new AnchorProvider(connection, userWallet, options); @@ -50,10 +48,8 @@ const main = async (): Promise => { // fetch group const group = await client.getGroup(GROUP); // liquidator's mango account - const liquidatorMangoAccount = await client.getMangoAccountForOwner( - group, - keypair.publicKey, - parseFloat(LIQUIDATOR_MANGO_ACCT_NUM), + const liquidatorMangoAccount = await client.getMangoAccount( + new PublicKey(MANGO_ACCOUNT_PK), ); if (!liquidatorMangoAccount) { throw new Error('liquidatorMangoAccount not found'); From 141288f016f7c1855d594413354c81282f9601b6 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Wed, 10 Jan 2024 11:46:03 +0100 Subject: [PATCH 3/3] --wip-- [skip ci] --- .../scripts/liquidator/create-candidate.ts | 119 ++++++++++++++++++ ts/client/scripts/liquidator/index.ts | 56 +++++++-- 2 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 ts/client/scripts/liquidator/create-candidate.ts diff --git a/ts/client/scripts/liquidator/create-candidate.ts b/ts/client/scripts/liquidator/create-candidate.ts new file mode 100644 index 000000000..69ce5d03e --- /dev/null +++ b/ts/client/scripts/liquidator/create-candidate.ts @@ -0,0 +1,119 @@ +import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; +import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; +import fs from 'fs'; +import { TokenIndex } from '../../src/accounts/bank'; +import { + Serum3OrderType, + Serum3SelfTradeBehavior, + Serum3Side, +} from '../../src/accounts/serum3'; +import { MangoClient } from '../../src/client'; +import { MANGO_V4_ID } from '../../src/constants'; +import { I80F48 } from '../../src/numbers/I80F48'; +import { toUiDecimalsForQuote } from '../../src/utils'; + +const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'); +const CLUSTER = process.env.CLUSTER || 'mainnet-beta'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const USER_KEYPAIR = + process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; +const LIAB_LIMIT = I80F48.fromNumber( + Math.min(parseFloat(process.env.LIAB_LIMIT || '0.9'), 1), +); + +const main = async (): Promise => { + const options = AnchorProvider.defaultOptions(); + options.commitment = 'processed'; + options.preflightCommitment = 'finalized'; + const connection = new Connection(CLUSTER_URL!, options); + + const keypair = Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))), + ); + const userWallet = new Wallet(keypair); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = MangoClient.connect( + userProvider, + CLUSTER as Cluster, + MANGO_V4_ID[CLUSTER], + { + idsSource: 'api', + prioritizationFee: 100, + txConfirmationCommitment: 'confirmed', + }, + ); + + // fetch group + const group = await client.getGroup(GROUP); + // liquidator's mango account + const liqee = await client.getMangoAccount(new PublicKey(MANGO_ACCOUNT_PK)); + await client.tokenDeposit( + group, + liqee, + group.getFirstBankByTokenIndex(0 as TokenIndex).mint, + 10, + ); + + let stopTryingSpot = false; + let withdraw = 1e5; + // eslint-disable-next-line no-constant-condition + while (true) { + await group.reloadAll(client); + + if (!stopTryingSpot) { + try { + const serum3Market = group.getSerum3MarketByName('BONK/USDC'); + const external = group.getSerum3ExternalMarket( + serum3Market.serumMarketExternal, + ); + await client.serum3PlaceOrder( + group, + liqee, + serum3Market.serumMarketExternal, + Serum3Side.bid, + group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).uiPrice * + 1.1, + external.baseSizeLotsToNumber(new BN(1)), + Serum3SelfTradeBehavior.decrementTake, + Serum3OrderType.immediateOrCancel, + new Date().valueOf(), + 10, + ); + } catch (error) { + stopTryingSpot = true; + } + } + + try { + await client.tokenWithdrawNative( + group, + liqee, + group.getFirstBankByTokenIndex(0 as TokenIndex).mint, + new BN(withdraw), + true, + ); + } catch (error) { + if (withdraw >= 1) { + withdraw = withdraw / 2; + } else { + throw error; + } + } + + await liqee.reload(client); + + // console.log(`...Equity - ${toUiDecimalsForQuote(liqee.getEquity(group))}`); + console.log( + `...Maint health - ${toUiDecimalsForQuote( + liqee.getHealth(group, 'Maint'), + )}`, + ); + // console.log(``); + + await new Promise((r) => setTimeout(r, 100)); + } +}; + +main(); diff --git a/ts/client/scripts/liquidator/index.ts b/ts/client/scripts/liquidator/index.ts index c335e91db..4f0e72e96 100644 --- a/ts/client/scripts/liquidator/index.ts +++ b/ts/client/scripts/liquidator/index.ts @@ -10,6 +10,7 @@ import { MarketIndex } from '../../src/accounts/serum3'; import { MangoClient } from '../../src/client'; import { MANGO_V4_ID } from '../../src/constants'; import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../../src/numbers/I80F48'; +import { toUiDecimalsForQuote } from '../../src/utils'; import { MangoSignatureStatus } from '../../src/utils/rpc'; const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'); @@ -39,7 +40,7 @@ const main = async (): Promise => { CLUSTER as Cluster, MANGO_V4_ID[CLUSTER], { - idsSource: 'get-program-accounts', + idsSource: 'api', prioritizationFee: 100, txConfirmationCommitment: 'confirmed', }, @@ -54,29 +55,58 @@ const main = async (): Promise => { if (!liquidatorMangoAccount) { throw new Error('liquidatorMangoAccount not found'); } - const mangoAccounts = await client.getAllMangoAccounts(group, true); - // loop over all mangoAccounts and find liquidable ones - for (const mangoAccount of mangoAccounts) { - if (!isLiquidable(mangoAccount, group)) { - continue; - } + // eslint-disable-next-line no-constant-condition + while (true) { + console.log(``); + + await group.reloadAll(client); + let mangoAccounts = await client.getAllMangoAccounts(group, true); + + mangoAccounts = mangoAccounts.filter( + (a) => toUiDecimalsForQuote(a.getEquity(group)) > 0, + ); - console.log(`Attempting to liquidate ${mangoAccount.publicKey.toBase58()}`); + mangoAccounts.sort((a, b) => + a.getHealth(group, 'maint').gt(b.getHealth(group, 'maint')) ? 1 : -1, + ); + + // loop over all mangoAccounts and find liquidable ones + for (const mangoAccount of mangoAccounts) { + if (!isLiquidable(mangoAccount, group)) { + continue; + } - try { - liquidateAccount(mangoAccount, liquidatorMangoAccount, group, client); - } catch (e) { - console.error( - `Error liquidating ${mangoAccount.publicKey.toBase58()}, ${e}`, + console.log( + `Attempting to liquidate ${mangoAccount.publicKey.toBase58()}`, ); + + try { + await liquidateAccount( + mangoAccount, + liquidatorMangoAccount, + group, + client, + ); + } catch (e) { + console.error( + `Error liquidating ${mangoAccount.publicKey.toBase58()}, ${e}`, + ); + } } + + await new Promise((r) => setTimeout(r, 500)); } }; main(); function isLiquidable(mangoAccount: MangoAccount, group: Group): boolean { + // console.log( + // ` ${mangoAccount.publicKey} ${toUiDecimalsForQuote( + // mangoAccount.getHealth(group, 'Maint'), + // )}`, + // ); return ( mangoAccount.getHealth(group, 'Init').isNeg() || mangoAccount.getHealth(group, 'Maint').isNeg()