import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; import * as splToken from '@solana/spl-token'; import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; import { Bank } from '../../src/accounts/bank'; import { MangoAccount, TokenConditionalSwapDisplayPriceStyle, TokenConditionalSwapIntention, } from '../../src/accounts/mangoAccount'; import { PerpMarket } from '../../src/accounts/perp'; import { Builder } from '../../src/builder'; import { MangoClient } from '../../src/client'; import { DefaultTokenRegisterParams, 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); const CLUSTER = process.env.CLUSTER || 'mainnet-beta'; // native prices const PRICES = { ETH: 1200.0, SOL: 0.015, USDC: 1, MNGO: 0.02, }; const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [ [ 'TCS, FUNDING', [ ['USDC', 5000000], ['ETH', 100000], ['SOL', 150000000], ], [], ], ['TCS, LIQOR', [['USDC', 1000000]], []], ['TCS, HEALTH', [['USDC', 1000]], []], ['TCS, FULL+CLOSE', [['USDC', 1000000]], []], ['TCS, EXPIRE', [['USDC', 1000000]], []], ['TCS, NET-BORROW', [['USDC', 10000000]], []], ]; 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.PAYER_KEYPAIR!, 'utf-8')), ), ); const userWallet = new Wallet(admin); const userProvider = new AnchorProvider(connection, userWallet, options); const client = await MangoClient.connect( userProvider, CLUSTER as Cluster, MANGO_V4_ID[CLUSTER], { 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 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); const account = (await client.getMangoAccountForOwner( group, admin.publicKey, accountNum, ))!; await client.accountExpandV2(group, account, 4, 4, 4, 4, 50); await account.reload(client); return account; } 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...`); const mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); for (const [assetName, assetAmount] of assets) { const assetMint = new PublicKey(MINTS.get(assetName)!); await client.tokenDepositNative( group, mangoAccount, assetMint, new BN(assetAmount), ); await mangoAccount.reload(client); } for (const [liabName, liabAmount] of liabs) { const liabMint = new PublicKey(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, ); // Case LIQEE1: The liqee does not have enough health for the tcs { const account = ensure( accounts2.find((account) => account.name == 'TCS, HEALTH'), ); await client.tokenConditionalSwapCreateRaw( group, account, MINTS.get('SOL')!, MINTS.get('USDC')!, new BN(100000000), new BN(20000), // liqee only has 1k USDC-native, leverage does not go that far! null, 0.0, 1000000.0, 0.01, true, true, TokenConditionalSwapDisplayPriceStyle.buyTokenPerSellToken, TokenConditionalSwapIntention.unknown, ); } // Case LIQEE2: Full execution - tcs closes afterward { const account = ensure( accounts2.find((account) => account.name == 'TCS, FULL+CLOSE'), ); await client.tokenConditionalSwapCreateRaw( group, account, MINTS.get('SOL')!, MINTS.get('USDC')!, new BN(1000), new BN(1000), null, 0.0, 1000000.0, 0.01, true, true, TokenConditionalSwapDisplayPriceStyle.buyTokenPerSellToken, TokenConditionalSwapIntention.unknown, ); } // Case LIQEE3: Create a tcs that will expire very soon { const account = ensure( accounts2.find((account) => account.name == 'TCS, EXPIRE'), ); await client.tokenConditionalSwapCreateRaw( group, account, MINTS.get('SOL')!, MINTS.get('USDC')!, new BN(1000), new BN(1000), Date.now() / 1000 + 15, // expire in 15s 0.0, 1000000.0, 0.01, true, true, TokenConditionalSwapDisplayPriceStyle.buyTokenPerSellToken, TokenConditionalSwapIntention.unknown, ); } // Case LIQEE4: Create a tcs that hits net borrow limits { const account = ensure( accounts2.find((account) => account.name == 'TCS, NET-BORROW'), ); // To do this, first make a new mint and register it as a new token with tight net borrow limits const newMint = await splToken.createMint( connection, admin, admin.publicKey, null, 6, ); const tokenAccount = await splToken.createAssociatedTokenAccountIdempotent( connection, admin, newMint, admin.publicKey, ); await splToken.mintTo( connection, admin, newMint, tokenAccount, admin, 1e15, ); await client.stubOracleCreate(group, newMint, 1.0); const newOracle = (await client.getStubOracle(group, newMint))[0]; const newTokenIndex = Math.max(...group.banksMapByTokenIndex.keys()) + 1; await client.tokenRegister( group, newMint, newOracle.publicKey, PublicKey.default, newTokenIndex, 'TMP', { ...DefaultTokenRegisterParams, loanOriginationFeeRate: 0, loanFeeRate: 0, initAssetWeight: 1, maintAssetWeight: 1, initLiabWeight: 1, maintLiabWeight: 1, liquidationFee: 0, netBorrowLimitPerWindowQuote: 1500000, // less than the $2 of the tcs }, ); await group.reloadAll(client); await client.tokenConditionalSwapCreateRaw( group, account, newMint, MINTS.get('USDC')!, new BN(2000000), // $2 new BN(2000000), null, 0.0, 1000000.0, 0.01, true, true, TokenConditionalSwapDisplayPriceStyle.buyTokenPerSellToken, TokenConditionalSwapIntention.unknown, ); } process.exit(); } function ensure(value: T | undefined): T { if (value == null) { throw new Error('Value was nullish'); } return value; } main();