import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; import { Bank } from '../src/accounts/bank'; import { MangoAccount } from '../src/accounts/mangoAccount'; import { PerpMarket, PerpOrderSide, PerpOrderType } from '../src/accounts/perp'; import { Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side, } from '../src/accounts/serum3'; import { Builder } from '../src/builder'; import { MangoClient } from '../src/client'; import { NullPerpEditParams, NullTokenEditParams, } from '../src/clientIxParamBuilder'; import { MANGO_V4_ID } from '../src/constants'; // // This script creates liquidation candidates // const GROUP_NUM = Number(process.env.GROUP_NUM || 200); // native prices const PRICES = { ETH: 1200.0, SOL: 0.015, USDC: 1, MNGO: 0.02, }; const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [ [ 'LIQTEST, FUNDING', [ ['USDC', 5000000], ['ETH', 100000], ['SOL', 150000000], ], [], ], ['LIQTEST, LIQOR', [['USDC', 1000000]], []], ['LIQTEST, A: USDC, L: SOL', [['USDC', 1000 * PRICES.SOL]], [['SOL', 920]]], ['LIQTEST, A: SOL, L: USDC', [['SOL', 1000]], [['USDC', 990 * PRICES.SOL]]], [ 'LIQTEST, A: ETH, L: SOL', [['ETH', 20]], [['SOL', (18 * PRICES.ETH) / PRICES.SOL]], ], ]; async function main() { const options = AnchorProvider.defaultOptions(); options.commitment = 'processed'; options.preflightCommitment = 'finalized'; const connection = new Connection(process.env.CLUSTER_URL!, options); const admin = Keypair.fromSecretKey( Buffer.from( JSON.parse( fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'), ), ), ); const userWallet = new Wallet(admin); const userProvider = new AnchorProvider(connection, userWallet, options); const client = await MangoClient.connect( userProvider, 'mainnet-beta', MANGO_V4_ID['mainnet-beta'], { idsSource: 'get-program-accounts', prioritizationFee: 100, txConfirmationCommitment: 'confirmed', }, ); console.log(`User ${userWallet.publicKey.toBase58()}`); // fetch group const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); console.log(group.toString()); const MAINNET_MINTS = new Map([ ['USDC', group.banksMapByName.get('USDC')![0].mint], ['ETH', group.banksMapByName.get('ETH')![0].mint], ['SOL', group.banksMapByName.get('SOL')![0].mint], ]); const accounts = await client.getMangoAccountsForOwner( group, admin.publicKey, ); let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum)); async function createMangoAccount(name: string): Promise { const accountNum = maxAccountNum + 1; maxAccountNum = maxAccountNum + 1; await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4); return (await client.getMangoAccountForOwner( group, admin.publicKey, accountNum, ))!; } async function setBankPrice(bank: Bank, price: number): Promise { await client.stubOracleSet(group, bank.oracle, price); // reset stable price await client.tokenEdit( group, bank.mint, Builder(NullTokenEditParams).resetStablePrice(true).build(), ); } async function setPerpPrice( perpMarket: PerpMarket, price: number, ): Promise { await client.stubOracleSet(group, perpMarket.oracle, price); // reset stable price await client.perpEditMarket( group, perpMarket.perpMarketIndex, Builder(NullPerpEditParams).resetStablePrice(true).build(), ); } for (const scenario of TOKEN_SCENARIOS) { const [name, assets, liabs] = scenario; // create account console.log(`Creating mangoaccount...`); let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); for (let [assetName, assetAmount] of assets) { const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!); await client.tokenDepositNative( group, mangoAccount, assetMint, new BN(assetAmount), ); await mangoAccount.reload(client); } for (let [liabName, liabAmount] of liabs) { const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!); // temporarily drop the borrowed token value, so the borrow goes through const bank = group.banksMapByName.get(liabName)![0]; try { await setBankPrice(bank, PRICES[liabName] / 2); await client.tokenWithdrawNative( group, mangoAccount, liabMint, new BN(liabAmount), true, ); } finally { // restore the oracle await setBankPrice(bank, PRICES[liabName]); } } } const accounts2 = await client.getMangoAccountsForOwner( group, admin.publicKey, ); const fundingAccount = accounts2.find( (account) => account.name == 'LIQTEST, FUNDING', ); if (!fundingAccount) { throw new Error('could not find funding account'); } // Serum order scenario { const name = 'LIQTEST, serum orders'; console.log(`Creating mangoaccount...`); let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); const market = group.getSerum3MarketByName('SOL/USDC')!; const sellMint = new PublicKey(MAINNET_MINTS.get('USDC')!); const buyMint = new PublicKey(MAINNET_MINTS.get('SOL')!); await client.tokenDepositNative( group, mangoAccount, sellMint, new BN(100000), ); await mangoAccount.reload(client); // temporarily up the init asset weight of the bought token await client.tokenEdit( group, buyMint, Builder(NullTokenEditParams) .oracle(group.getFirstBankByMint(buyMint).oracle) .maintAssetWeight(1.0) .initAssetWeight(1.0) .build(), ); try { // At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have. // With maint weight of 0.9 we have 10x main-leverage. Buying 12x as much causes liquidation. await client.serum3PlaceOrder( group, mangoAccount, market.serumMarketExternal, Serum3Side.bid, 1, 12 * 0.1, Serum3SelfTradeBehavior.abortTransaction, Serum3OrderType.limit, 0, 5, ); } finally { // restore the weights await client.tokenEdit( group, buyMint, Builder(NullTokenEditParams) .oracle(group.getFirstBankByMint(buyMint).oracle) .maintAssetWeight(0.9) .initAssetWeight(0.8) .build(), ); } } // Perp orders bring health <0, liquidator force closes { const name = 'LIQTEST, perp orders'; console.log(`Creating mangoaccount...`); let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, new BN(300000), ); // valued as 0.0003 SOL, $0.0045 maint collateral await mangoAccount.reload(client); await setBankPrice(collateralBank, PRICES['SOL'] * 4); try { // placing this order decreases maint health by (0.9 - 1)*$0.06 = $-0.006 await client.perpPlaceOrder( group, mangoAccount, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, PerpOrderSide.bid, 0.001, // ui price that won't get hit 3.0, // ui base quantity, 30 base lots, 3.0 MNGO, $0.06 0.06, // ui quote quantity 4200, PerpOrderType.limit, false, 0, 5, ); } finally { await setBankPrice(collateralBank, PRICES['SOL']); } } // Perp base pos brings health<0, liquidator takes most of it { const name = 'LIQTEST, perp base pos'; console.log(`Creating mangoaccount...`); let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, new BN(300000), ); // valued as 0.0003 SOL, $0.0045 maint collateral await mangoAccount.reload(client); await setBankPrice(collateralBank, PRICES['SOL'] * 10); try { await client.perpPlaceOrder( group, fundingAccount, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, PerpOrderSide.ask, 0.03, 1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033 0.033, // ui quote quantity 4200, PerpOrderType.limit, false, 0, 5, ); await client.perpPlaceOrder( group, mangoAccount, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, PerpOrderSide.bid, 0.03, 1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033 0.033, // ui quote quantity 4200, PerpOrderType.market, false, 0, 5, ); await client.perpConsumeAllEvents( group, group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!, ); } finally { await setBankPrice(collateralBank, PRICES['SOL']); } } // borrows and positive perp pnl (but no position) { const name = 'LIQTEST, perp positive pnl'; console.log(`Creating mangoaccount...`); let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!; const perpIndex = perpMarket.perpMarketIndex; const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, new BN(300000), ); // valued as $0.0045 maint collateral await mangoAccount.reload(client); try { await setBankPrice(collateralBank, PRICES['SOL'] * 10); // Spot-borrow more than the collateral is worth await client.tokenWithdrawNative( group, mangoAccount, liabMint, new BN(-5000), true, ); await mangoAccount.reload(client); // Execute two trades that leave the account with +$0.011 positive pnl await setPerpPrice(perpMarket, PRICES['MNGO'] / 2); await client.perpPlaceOrder( group, fundingAccount, perpIndex, PerpOrderSide.ask, 0.01, 1.1, // ui base quantity, 11 base lots, $0.011 0.011, // ui quote quantity 4200, PerpOrderType.limit, false, 0, 5, ); await client.perpPlaceOrder( group, mangoAccount, perpIndex, PerpOrderSide.bid, 0.01, 1.1, // ui base quantity, 11 base lots, $0.011 0.011, // ui quote quantity 4200, PerpOrderType.market, false, 0, 5, ); await client.perpConsumeAllEvents(group, perpIndex); await setPerpPrice(perpMarket, PRICES['MNGO']); await client.perpPlaceOrder( group, fundingAccount, perpIndex, PerpOrderSide.bid, 0.02, 1.1, // ui base quantity, 11 base lots, $0.022 0.022, // ui quote quantity 4201, PerpOrderType.limit, false, 0, 5, ); await client.perpPlaceOrder( group, mangoAccount, perpIndex, PerpOrderSide.ask, 0.02, 1.1, // ui base quantity, 11 base lots, $0.022 0.022, // ui quote quantity 4201, PerpOrderType.market, false, 0, 5, ); await client.perpConsumeAllEvents(group, perpIndex); } finally { await setPerpPrice(perpMarket, PRICES['MNGO']); await setBankPrice(collateralBank, PRICES['SOL']); } } // assets and negative perp pnl (but no position) { const name = 'LIQTEST, perp negative pnl'; console.log(`Creating mangoaccount...`); let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!; const perpIndex = perpMarket.perpMarketIndex; const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!); const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); const collateralBank = group.banksMapByName.get('SOL')![0]; await client.tokenDepositNative( group, mangoAccount, collateralMint, new BN(300000), ); // valued as $0.0045 maint collateral await mangoAccount.reload(client); try { await setBankPrice(collateralBank, PRICES['SOL'] * 10); // Execute two trades that leave the account with -$0.011 negative pnl await setPerpPrice(perpMarket, PRICES['MNGO'] / 2); await client.perpPlaceOrder( group, fundingAccount, perpIndex, PerpOrderSide.bid, 0.01, 1.1, // ui base quantity, 11 base lots, $0.011 0.011, // ui quote quantity 4200, PerpOrderType.limit, false, 0, 5, ); await client.perpPlaceOrder( group, mangoAccount, perpIndex, PerpOrderSide.ask, 0.01, 1.1, // ui base quantity, 11 base lots, $0.011 0.011, // ui quote quantity 4200, PerpOrderType.market, false, 0, 5, ); await client.perpConsumeAllEvents(group, perpIndex); await setPerpPrice(perpMarket, PRICES['MNGO']); await client.perpPlaceOrder( group, fundingAccount, perpIndex, PerpOrderSide.ask, 0.02, 1.1, // ui base quantity, 11 base lots, $0.022 0.022, // ui quote quantity 4201, PerpOrderType.limit, false, 0, 5, ); await client.perpPlaceOrder( group, mangoAccount, perpIndex, PerpOrderSide.bid, 0.02, 1.1, // ui base quantity, 11 base lots, $0.022 0.022, // ui quote quantity 4201, PerpOrderType.market, false, 0, 5, ); await client.perpConsumeAllEvents(group, perpIndex); } finally { await setPerpPrice(perpMarket, PRICES['MNGO']); await setBankPrice(collateralBank, PRICES['SOL']); } } process.exit(); } main();