From 690e7097c3510812ddec53420ab1c628885a1559 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:32:49 +0200 Subject: [PATCH] Mc/force close (#1006) * foce close serum markets, add force close oo, and improve logging Signed-off-by: microwavedcola1 * force withdraw token script: improved logging Signed-off-by: microwavedcola1 * script for force close token borrows, simplify, add better logging Signed-off-by: microwavedcola1 --------- Signed-off-by: microwavedcola1 --- .../scripts/force-close-serum3-market.ts | 18 ++- .../scripts/force-close-token-borrows.ts | 116 ++++++------------ ts/client/scripts/force-withdraw-token.ts | 33 ++--- ts/client/src/accounts/group.ts | 6 + 4 files changed, 67 insertions(+), 106 deletions(-) diff --git a/ts/client/scripts/force-close-serum3-market.ts b/ts/client/scripts/force-close-serum3-market.ts index 266c86203..1dce9cd86 100644 --- a/ts/client/scripts/force-close-serum3-market.ts +++ b/ts/client/scripts/force-close-serum3-market.ts @@ -46,9 +46,9 @@ async function forceCloseSerum3Market(): Promise { ); for (let a of mangoAccounts) { + console.log(`mango account ${a.publicKey}`); // Cancel all orders and confirm that all have been cancelled for (const _ of range(0, 10)) { - console.log(a.getSerum3OoAccount(MARKET_INDEX).freeSlotBits.zeroBits()); const sig = await client.serum3LiqForceCancelOrders( group, a, @@ -56,17 +56,27 @@ async function forceCloseSerum3Market(): Promise { 10, ); console.log( - ` serum3LiqForceCancelOrders for ${ - a.publicKey - }, sig https://explorer.solana.com/tx/${sig.signature}?cluster=${ + ` - serum3LiqForceCancelOrders https://explorer.solana.com/tx/${sig.signature}?cluster=${ CLUSTER == 'devnet' ? 'devnet' : '' }`, ); + a = await a.reload(client); if (a.getSerum3OoAccount(MARKET_INDEX).freeSlotBits.zeroBits() === 0) { break; } } + + const sig2 = await client.serum3CloseOpenOrders( + group, + a, + serum3Market.serumMarketExternal, + ); + console.log( + ` - serum3CloseOpenOrders https://explorer.solana.com/tx/${sig2.signature}?cluster=${ + CLUSTER == 'devnet' ? 'devnet' : '' + }`, + ); } } diff --git a/ts/client/scripts/force-close-token-borrows.ts b/ts/client/scripts/force-close-token-borrows.ts index 6041c8364..1ed4dcc04 100644 --- a/ts/client/scripts/force-close-token-borrows.ts +++ b/ts/client/scripts/force-close-token-borrows.ts @@ -2,14 +2,10 @@ import { AnchorProvider, 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 { HealthType } from '../src/accounts/mangoAccount'; import { MangoClient } from '../src/client'; import { MANGO_V4_ID } from '../src/constants'; -import { - fetchJupiterTransaction, - fetchRoutes, - prepareMangoRouterInstructions, -} from '../src/router'; -import { toNative, toUiDecimals } from '../src/utils'; +import { ONE_I80F48 } from '../src/numbers/I80F48'; const CLUSTER: Cluster = (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; @@ -56,96 +52,62 @@ async function forceCloseTokenBorrows(): Promise { ); } - const usdcBank = group.getFirstBankByTokenIndex(0 as TokenIndex); - // Get all mango accounts with borrows for given token const mangoAccountsWithBorrows = ( await client.getAllMangoAccounts(group) ).filter((a) => a.getTokenBalanceUi(forceCloseTokenBank) < 0); + if ( + forceCloseTokenBank.uiBorrows() >= + liqor.getTokenBalanceUi(forceCloseTokenBank) + ) { + throw new Error( + `Ensure that liqor has enough deposits to cover borrows! forceCloseTokenBank.uiBorrows() ${forceCloseTokenBank.uiBorrows()}, liqor.getTokenBalanceUi(forceCloseTokenBank) ${liqor.getTokenBalanceUi(forceCloseTokenBank)}`, + ); + } + console.log(`${liqor.toString(group, true)}`); for (const liqee of mangoAccountsWithBorrows) { liqor = await liqor.reload(client); - // Liqor can only liquidate borrow using deposits, since borrows are in reduce only - // Swap usdc worth token borrow (sub existing position), account for slippage using liquidation fee - // MAX_LIAB_TRANSFER guards against trying to swap to a very large amount - const amount = - Math.min( - liqee.getTokenBorrowsUi(forceCloseTokenBank) - - liqor.getTokenBalanceUi(forceCloseTokenBank), - MAX_LIAB_TRANSFER, - ) * - forceCloseTokenBank.uiPrice * - (1 + forceCloseTokenBank.liquidationFee.toNumber()); - console.log( - `liqor balance ${liqor.getTokenBalanceUi( - forceCloseTokenBank, - )}, liqee balance ${liqee.getTokenBalanceUi( - forceCloseTokenBank, - )}, liqor will swap further amount of $${toUiDecimals( - amount, - usdcBank.mintDecimals, - )} to ${forceCloseTokenBank.name}`, - ); + const sortedByContribution = liqee + .getHealthContributionPerAssetUi(group, HealthType.init) + .filter((a) => { + const potentialAssetBank = group.getFirstBankByName(a.asset); - const amountBn = toNative( - Math.min(amount, 99999999999), // Jupiter API can't handle amounts larger than 99999999999 - usdcBank.mintDecimals, - ); - const { bestRoute } = await fetchRoutes( - usdcBank.mint, - forceCloseTokenBank.mint, - amountBn.toString(), - forceCloseTokenBank.liquidationFee.toNumber() * 100, - 'ExactIn', - '0', - liqor.owner, - ); - if (!bestRoute) { - await new Promise((r) => setTimeout(r, 500)); - continue; - } - const [ixs, alts] = - bestRoute.routerName === 'Mango' - ? await prepareMangoRouterInstructions( - bestRoute, - usdcBank.mint, - forceCloseTokenBank.mint, - user.publicKey, - ) - : await fetchJupiterTransaction( - client.connection, - bestRoute, - user.publicKey, - 0, - usdcBank.mint, - forceCloseTokenBank.mint, + const feeFactorTotal = ONE_I80F48() + .add(forceCloseTokenBank.liquidationFee) + .add(forceCloseTokenBank.platformLiquidationFee) + .mul( + ONE_I80F48() + .add(potentialAssetBank.liquidationFee) + .add(potentialAssetBank.platformLiquidationFee), ); - const sig = await client.marginTrade({ - group: group, - mangoAccount: liqor, - inputMintPk: usdcBank.mint, - amountIn: amount, - outputMintPk: forceCloseTokenBank.mint, - userDefinedInstructions: ixs, - userDefinedAlts: alts, - flashLoanType: { swap: {} }, - sequenceCheck: false, - }); + + return ( + potentialAssetBank.reduceOnly != 2 && + forceCloseTokenBank.initLiabWeight.gte( + potentialAssetBank.initLiabWeight.mul(feeFactorTotal), + ) + ); + }) + .sort((a, b) => { + return a.contribution - b.contribution; + }); + const assetBank = group.getFirstBankByName(sortedByContribution[0].asset); + console.log( - ` - marginTrade, sig https://explorer.solana.com/tx/${sig}?cluster=${ - CLUSTER == 'devnet' ? 'devnet' : '' - }`, + `${liqee.publicKey.toString()}, balance ${liqee.getTokenBalanceUi(forceCloseTokenBank)}, asset ${assetBank.name}, contribution ${sortedByContribution[0].contribution}`, ); - await client.tokenForceCloseBorrowsWithToken( + const sig = await client.tokenForceCloseBorrowsWithToken( group, liqor, liqee, - usdcBank.tokenIndex, + assetBank.tokenIndex, forceCloseTokenBank.tokenIndex, ); + console.log(` - sig ${sig.signature}`); } } diff --git a/ts/client/scripts/force-withdraw-token.ts b/ts/client/scripts/force-withdraw-token.ts index 517d37118..eb3298b7f 100644 --- a/ts/client/scripts/force-withdraw-token.ts +++ b/ts/client/scripts/force-withdraw-token.ts @@ -65,43 +65,26 @@ async function forceWithdrawTokens(): Promise { for (const mangoAccount of mangoAccounts) { console.log( - `${mangoAccount.getTokenBalanceUi(forceWithdrawBank)} for ${ - mangoAccount.publicKey - }`, + `${mangoAccount.publicKey} ${forceWithdrawBank.name} balance ${mangoAccount.getTokenBalanceUi(forceWithdrawBank)}`, ); - try { - const sig = await client.serum3LiqForceCancelOrders( - group, - mangoAccount, - serum3Market.serumMarketExternal, - ); - console.log( - ` serum3LiqForceCancelOrders for ${mangoAccount.publicKey}, owner ${ - mangoAccount.owner - }, sig https://explorer.solana.com/tx/${sig.signature}?cluster=${ - CLUSTER == 'devnet' ? 'devnet' : '' - }`, - ); - } catch (error) { - console.log(error); - } - await client .tokenForceWithdraw(group, mangoAccount, TOKEN_INDEX) .then((sig) => { console.log( - ` tokenForceWithdraw for ${mangoAccount.publicKey}, owner ${ - mangoAccount.owner - }, sig https://explorer.solana.com/tx/${sig.signature}?cluster=${ + ` - tokenForceWithdraw https://explorer.solana.com/tx/${sig.signature}?cluster=${ CLUSTER == 'devnet' ? 'devnet' : '' }`, ); }); } - await group.reloadAll(client); - console.log(forceWithdrawBank.uiDeposits()); + const groupFresh = await client.getGroup(new PublicKey(GROUP_PK)); + const forceWithdrawBankFresh = + groupFresh.getFirstBankByTokenIndex(TOKEN_INDEX); + console.log( + `Final ${forceWithdrawBankFresh.name} deposits ${forceWithdrawBankFresh.uiDeposits()}`, + ); } forceWithdrawTokens(); diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 914b8771a..ddfbf3b1f 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -598,6 +598,12 @@ export class Group { return banks[0]; } + public getFirstBankByName(name: string): Bank { + const banks = this.banksMapByName.get(name); + if (!banks) throw new Error(`No bank found for name ${name}!`); + return banks[0]; + } + public getFirstBankByOracle(oraclePk: PublicKey): Bank { const banks = this.banksMapByOracle.get(oraclePk.toString()); if (!banks) throw new Error(`No bank found for oracle ${oraclePk}!`);