From 9cbc352197f890fef1501e36490b09fa009e432a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 15 Sep 2022 09:57:48 +0200 Subject: [PATCH] liquidator: force-cancel perp orders, liq perp base positions --- client/src/client.rs | 96 +++++++++++- liquidator/src/liquidate.rs | 95 +++++++++++- liquidator/src/main.rs | 3 + ts/client/src/client.ts | 133 ++++++++++++++++- .../src/scripts/mb-example1-admin-close.ts | 18 +-- .../src/scripts/mb-liqtest-create-group.ts | 52 ++++++- .../src/scripts/mb-liqtest-deposit-tokens.ts | 19 ++- .../src/scripts/mb-liqtest-make-candidates.ts | 138 ++++++++++++++++-- .../mb-liqtest-settle-and-close-all.ts | 60 +------- 9 files changed, 512 insertions(+), 102 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 6f9f56b15..0170f7bed 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -15,14 +15,16 @@ use bincode::Options; use fixed::types::I80F48; use itertools::Itertools; use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; -use mango_v4::state::{Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex}; +use mango_v4::state::{ + Bank, Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, +}; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_client::RpcClient; use solana_sdk::signer::keypair; use crate::account_fetcher::*; -use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext}; +use crate::context::{MangoGroupContext, PerpMarketContext, Serum3MarketContext, TokenContext}; use crate::gpa::fetch_mango_accounts; use crate::jupiter; use crate::util::MyClone; @@ -836,12 +838,98 @@ impl MangoClient { // // Perps // + fn perp_data_by_market_index( + &self, + market_index: PerpMarketIndex, + ) -> Result<&PerpMarketContext, ClientError> { + Ok(self.context.perp_markets.get(&market_index).unwrap()) + } + + pub fn perp_liq_force_cancel_orders( + &self, + liqee: (&Pubkey, &MangoAccountValue), + market_index: PerpMarketIndex, + ) -> anyhow::Result { + let perp = self.perp_data_by_market_index(market_index)?; + + let health_remaining_ams = self + .context + .derive_health_check_remaining_account_metas(liqee.1, vec![], false) + .unwrap(); + + self.program() + .request() + .instruction(Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqForceCancelOrders { + group: self.group(), + account: *liqee.0, + perp_market: perp.address, + asks: perp.market.asks, + bids: perp.market.bids, + oracle: perp.market.oracle, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpLiqForceCancelOrders { limit: 5 }, + ), + }) + .send() + .map_err(prettify_client_error) + } + + pub fn perp_liq_base_position( + &self, + liqee: (&Pubkey, &MangoAccountValue), + market_index: PerpMarketIndex, + max_base_transfer: i64, + ) -> anyhow::Result { + let perp = self.perp_data_by_market_index(market_index)?; + + let health_remaining_ams = self + .context + .derive_health_check_remaining_account_metas(liqee.1, vec![], false) + .unwrap(); + + self.program() + .request() + .instruction(Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqBasePosition { + group: self.group(), + perp_market: perp.address, + oracle: perp.market.oracle, + liqor: self.mango_account_address, + liqor_owner: self.owner(), + liqee: *liqee.0, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpLiqBasePosition { max_base_transfer }, + ), + }) + .signer(&self.owner) + .send() + .map_err(prettify_client_error) + } // // Liquidation // - pub fn liq_token_with_token( + pub fn token_liq_with_token( &self, liqee: (&Pubkey, &MangoAccountValue), asset_token_index: TokenIndex, @@ -886,7 +974,7 @@ impl MangoClient { .map_err(prettify_client_error) } - pub fn liq_token_bankruptcy( + pub fn token_liq_bankruptcy( &self, liqee: (&Pubkey, &MangoAccountValue), liab_token_index: TokenIndex, diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 3986a830e..1572d2614 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -5,7 +5,7 @@ use crate::account_shared_data::KeyedAccountSharedData; use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; use mango_v4::state::{ new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue, - Serum3Orders, TokenIndex, QUOTE_TOKEN_INDEX, + PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX, }; use itertools::Itertools; @@ -177,6 +177,36 @@ pub fn maybe_liquidate_account( .filter_map_ok(|v| v) .collect::>>()?; + // look for any perp open orders and base positions + let perp_force_cancels = account + .active_perp_positions() + .filter_map(|pp| pp.has_open_orders().then(|| pp.market_index)) + .collect::>(); + let mut perp_base_positions = account + .active_perp_positions() + .map(|pp| { + let base_lots = pp.base_position_lots(); + if base_lots == 0 { + return Ok(None); + } + let perp = mango_client.context.perp(pp.market_index); + let oracle = account_fetcher.fetch_raw_account(perp.market.oracle)?; + let price = perp.market.oracle_price(&KeyedAccountSharedData::new( + perp.market.oracle, + oracle.into(), + ))?; + Ok(Some(( + pp.market_index, + base_lots, + price, + I80F48::from(base_lots.abs()) * price, + ))) + }) + .filter_map_ok(|v| v) + .collect::>>()?; + // sort by base_position_value, ascending + perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3)); + let get_max_liab_transfer = |source, target| -> anyhow::Result { let mut liqor = account_fetcher .fetch_fresh_mango_account(&mango_client.mango_account_address) @@ -203,7 +233,7 @@ pub fn maybe_liquidate_account( // try liquidating let txsig = if !serum_force_cancels.is_empty() { - // pick a random market to force-cancel orders on + // Cancel all orders on a random serum market let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap(); let sig = mango_client.serum3_liq_force_cancel_orders( (pubkey, &account), @@ -211,13 +241,68 @@ pub fn maybe_liquidate_account( &serum_orders.open_orders, )?; log::info!( - "Force cancelled serum market on account {}, market index {}, maint_health was {}, tx sig {:?}", + "Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}", pubkey, serum_orders.market_index, maint_health, sig ); sig + } else if !perp_force_cancels.is_empty() { + // Cancel all orders on a random perp market + let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap(); + let sig = + mango_client.perp_liq_force_cancel_orders((pubkey, &account), perp_market_index)?; + log::info!( + "Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}", + pubkey, + perp_market_index, + maint_health, + sig + ); + sig + } else if !perp_base_positions.is_empty() { + // Liquidate the highest-value perp base position + let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap(); + let perp = mango_client.context.perp(*perp_market_index); + + let (side, side_signum) = if *base_lots > 0 { + (Side::Bid, 1) + } else { + (Side::Ask, -1) + }; + + // Compute the max number of base_lots the liqor is willing to take + let max_base_transfer_abs = { + let mut liqor = account_fetcher + .fetch_fresh_mango_account(&mango_client.mango_account_address) + .context("getting liquidator account")?; + liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; + let health_cache = new_health_cache_(&mango_client.context, account_fetcher, &liqor) + .expect("always ok"); + health_cache.max_perp_for_health_ratio( + *perp_market_index, + *price, + perp.market.base_lot_size, + side, + min_health_ratio, + )? + }; + log::info!("computed max_base_transfer to be {max_base_transfer_abs}"); + + let sig = mango_client.perp_liq_base_position( + (pubkey, &account), + *perp_market_index, + side_signum * max_base_transfer_abs, + )?; + log::info!( + "Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", + pubkey, + perp_market_index, + maint_health, + sig + ); + sig } else if is_spot_bankrupt { if tokens.is_empty() { anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey); @@ -240,7 +325,7 @@ pub fn maybe_liquidate_account( let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?; let sig = mango_client - .liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer) + .token_liq_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer) .context("sending liq_token_bankruptcy")?; log::info!( "Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}", @@ -288,7 +373,7 @@ pub fn maybe_liquidate_account( // TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side // let sig = mango_client - .liq_token_with_token( + .token_liq_with_token( (pubkey, &account), asset_token_index, liab_token_index, diff --git a/liquidator/src/main.rs b/liquidator/src/main.rs index c8aa90fc4..faf84ce1c 100644 --- a/liquidator/src/main.rs +++ b/liquidator/src/main.rs @@ -8,6 +8,7 @@ use client::{chain_data, keypair_from_cli, Client, MangoClient, MangoGroupContex use log::*; use mango_v4::state::{PerpMarketIndex, TokenIndex}; +use itertools::Itertools; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; use std::collections::HashSet; @@ -126,6 +127,8 @@ async fn main() -> anyhow::Result<()> { .tokens .values() .map(|value| value.mint_info.oracle) + .chain(group_context.perp_markets.values().map(|p| p.market.oracle)) + .unique() .collect::>(); // diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index b091cc613..a881ce8e5 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -34,7 +34,14 @@ import { PerpPosition, } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; -import { PerpMarket, PerpOrderSide, PerpOrderType } from './accounts/perp'; +import { + PerpEventQueue, + PerpMarket, + PerpOrderType, + PerpOrderSide, + FillEvent, + OutEvent, +} from './accounts/perp'; import { generateSerum3MarketExternalVaultSignerAddress, Serum3Market, @@ -541,9 +548,20 @@ export class MangoClient { group: Group, accountNumber?: number, name?: string, + tokenCount?: number, + serum3Count?: number, + perpCount?: number, + perpOoCount?: number, ): Promise { const transaction = await this.program.methods - .accountCreate(accountNumber ?? 0, 8, 8, 0, 0, name ?? '') + .accountCreate( + accountNumber ?? 0, + tokenCount ?? 8, + serum3Count ?? 8, + perpCount ?? 0, + perpOoCount ?? 0, + name ?? '', + ) .accounts({ group: group.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, @@ -560,6 +578,32 @@ export class MangoClient { ); } + public async createAndFetchMangoAccount( + group: Group, + accountNumber?: number, + name?: string, + tokenCount?: number, + serum3Count?: number, + perpCount?: number, + perpOoCount?: number, + ): Promise { + const accNum = accountNumber ?? 0; + await this.createMangoAccount( + group, + accNum, + name, + tokenCount, + serum3Count, + perpCount, + perpOoCount, + ); + return await this.getMangoAccountForOwner( + group, + (this.program.provider as AnchorProvider).wallet.publicKey, + accNum, + ); + } + public async expandMangoAccount( group: Group, account: MangoAccount, @@ -1511,6 +1555,37 @@ export class MangoClient { ); } + async perpDeactivatePosition( + group: Group, + mangoAccount: MangoAccount, + perpMarketName: string, + ): Promise { + const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const healthRemainingAccounts: PublicKey[] = + this.buildHealthRemainingAccounts( + AccountRetriever.Fixed, + group, + [mangoAccount], + [], + [], + ); + return await this.program.methods + .perpDeactivatePosition() + .accounts({ + group: group.publicKey, + account: mangoAccount.publicKey, + perpMarket: perpMarket.publicKey, + owner: (this.program.provider as AnchorProvider).wallet.publicKey, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), + ), + ) + .rpc(); + } + async perpPlaceOrder( group: Group, mangoAccount: MangoAccount, @@ -1586,6 +1661,60 @@ export class MangoClient { .rpc(); } + async perpConsumeEvents( + group: Group, + perpMarketName: string, + accounts: PublicKey[], + limit: number, + ): Promise { + const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + return await this.program.methods + .perpConsumeEvents(new BN(limit)) + .accounts({ + group: group.publicKey, + perpMarket: perpMarket.publicKey, + eventQueue: perpMarket.eventQueue, + }) + .remainingAccounts( + accounts.map( + (pk) => + ({ pubkey: pk, isWritable: true, isSigner: false } as AccountMeta), + ), + ) + .rpc(); + } + + async perpConsumeAllEvents( + group: Group, + perpMarketName: string, + ): Promise { + const limit = 8; + const perpMarket = group.perpMarketsMap.get(perpMarketName)!; + const eventQueue = await perpMarket.loadEventQueue(this); + let unconsumedEvents = eventQueue.getUnconsumedEvents(); + while (unconsumedEvents.length > 0) { + const events = unconsumedEvents.splice(0, limit); + const accounts = events + .map((ev) => { + switch (ev.eventType) { + case PerpEventQueue.FILL_EVENT_TYPE: + const fill = ev; + return [fill.maker, fill.taker]; + case PerpEventQueue.OUT_EVENT_TYPE: + const out = ev; + return [out.owner]; + case PerpEventQueue.LIQUIDATE_EVENT_TYPE: + return []; + default: + throw new Error(`Unknown event with eventType ${ev.eventType}`); + } + }) + .flat(); + + await this.perpConsumeEvents(group, perpMarketName, accounts, limit); + } + } + public async marginTrade({ group, mangoAccount, diff --git a/ts/client/src/scripts/mb-example1-admin-close.ts b/ts/client/src/scripts/mb-example1-admin-close.ts index e17b95bef..90ee75f5b 100644 --- a/ts/client/src/scripts/mb-example1-admin-close.ts +++ b/ts/client/src/scripts/mb-example1-admin-close.ts @@ -45,15 +45,6 @@ async function main() { let sig; - // close stub oracles - const stubOracles = await client.getStubOracle(group); - for (const stubOracle of stubOracles) { - sig = await client.stubOracleClose(group, stubOracle.publicKey); - console.log( - `Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`, - ); - } - // close all banks for (const banks of group.banksMapByMint.values()) { sig = await client.tokenDeregister(group, banks[0].mint); @@ -81,6 +72,15 @@ async function main() { ); } + // close stub oracles + const stubOracles = await client.getStubOracle(group); + for (const stubOracle of stubOracles) { + sig = await client.stubOracleClose(group, stubOracle.publicKey); + console.log( + `Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`, + ); + } + // finally, close the group sig = await client.groupClose(group); console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`); diff --git a/ts/client/src/scripts/mb-liqtest-create-group.ts b/ts/client/src/scripts/mb-liqtest-create-group.ts index 9963ae067..e464cd6eb 100644 --- a/ts/client/src/scripts/mb-liqtest-create-group.ts +++ b/ts/client/src/scripts/mb-liqtest-create-group.ts @@ -16,12 +16,14 @@ const MAINNET_MINTS = new Map([ ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['SOL', 'So11111111111111111111111111111111111111112'], + ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], ]); const STUB_PRICES = new Map([ ['USDC', 1.0], ['BTC', 20000.0], // btc and usdc both have 6 decimals ['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL + ['MNGO', 0.04], // same price/decimals as SOL for convenience ]); // External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json @@ -179,14 +181,48 @@ async function main() { } console.log('Registering SOL/USDC serum market...'); - await client.serum3RegisterMarket( - group, - new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!), - group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)), - group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)), - 1, - 'SOL/USDC', - ); + try { + await client.serum3RegisterMarket( + group, + new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!), + group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)), + group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)), + 1, + 'SOL/USDC', + ); + } catch (error) { + console.log(error); + } + + console.log('Registering MNGO-PERP market...'); + const mngoMainnetOracle = oracles.get('MNGO'); + try { + await client.perpCreateMarket( + group, + mngoMainnetOracle, + 0, + 'MNGO-PERP', + 0.1, + 9, + 0, + 10, + 100000, // base lots + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + -0.001, + 0.002, + -0.1, + 0.1, + 10, + false, + false, + ); + } catch (error) { + console.log(error); + } process.exit(); } diff --git a/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts b/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts index 5eeaa6555..c351824a4 100644 --- a/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts +++ b/ts/client/src/scripts/mb-liqtest-deposit-tokens.ts @@ -39,11 +39,14 @@ async function main() { // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = (await client.getOrCreateMangoAccount( + const mangoAccount = (await client.createAndFetchMangoAccount( group, - admin.publicKey, ACCOUNT_NUM, 'LIQTEST, FUNDING', + 8, + 4, + 4, + 4, ))!; console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); @@ -54,16 +57,16 @@ async function main() { // deposit try { - console.log(`...depositing 10 USDC`); - await client.tokenDeposit(group, mangoAccount, usdcMint, 10); + console.log(`...depositing 5 USDC`); + await client.tokenDeposit(group, mangoAccount, usdcMint, 5); await mangoAccount.reload(client, group); - console.log(`...depositing 0.0004 BTC`); - await client.tokenDeposit(group, mangoAccount, btcMint, 0.0004); + console.log(`...depositing 0.0002 BTC`); + await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002); await mangoAccount.reload(client, group); - console.log(`...depositing 0.25 SOL`); - await client.tokenDeposit(group, mangoAccount, solMint, 0.25); + console.log(`...depositing 0.15 SOL`); + await client.tokenDeposit(group, mangoAccount, solMint, 0.15); await mangoAccount.reload(client, group); } catch (error) { console.log(error); diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index 2a698c6ec..f05febe3b 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -6,6 +6,8 @@ import { Serum3SelfTradeBehavior, Serum3Side, } from '../accounts/serum3'; +import { Side, PerpOrderType } from '../accounts/perp'; +import { MangoAccount } from '../accounts/mangoAccount'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; @@ -26,6 +28,7 @@ const MAINNET_MINTS = new Map([ ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['SOL', 'So11111111111111111111111111111111111111112'], + ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], ]); const TOKEN_SCENARIOS: [string, string, number, string, number][] = [ @@ -66,19 +69,30 @@ async function main() { admin.publicKey, ); let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum)); + const fundingAccount = accounts.find( + (account) => account.name == 'LIQTEST, FUNDING', + ); + if (!fundingAccount) { + throw new Error('could not find funding account'); + } + + 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, + ))!; + } for (const scenario of TOKEN_SCENARIOS) { const [name, assetName, assetAmount, liabName, liabAmount] = scenario; // create account console.log(`Creating mangoaccount...`); - let mangoAccount = (await client.getOrCreateMangoAccount( - group, - admin.publicKey, - maxAccountNum + 1, - name, - ))!; - maxAccountNum = maxAccountNum + 1; + let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); @@ -119,13 +133,7 @@ async function main() { const name = 'LIQTEST, serum orders'; console.log(`Creating mangoaccount...`); - let mangoAccount = (await client.getOrCreateMangoAccount( - group, - admin.publicKey, - maxAccountNum + 1, - name, - ))!; - maxAccountNum = maxAccountNum + 1; + let mangoAccount = await createMangoAccount(name); console.log( `...created mangoAccount ${mangoAccount.publicKey} for ${name}`, ); @@ -188,6 +196,108 @@ async function main() { } } + // 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 baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!); + const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); + const collateralOracle = group.banksMapByName.get('SOL')![0].oracle; + + await client.tokenDepositNative( + group, + mangoAccount, + collateralMint, + 100000, + ); // valued as $0.004 maint collateral + await mangoAccount.reload(client, group); + + await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4); + + try { + await client.perpPlaceOrder( + group, + mangoAccount, + 'MNGO-PERP', + Side.bid, + 1, // ui price that won't get hit + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4200, + PerpOrderType.limit, + 0, + 5, + ); + } finally { + await client.stubOracleSet(group, collateralOracle, 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 baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!); + const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!); + const collateralOracle = group.banksMapByName.get('SOL')![0].oracle; + + await client.tokenDepositNative( + group, + mangoAccount, + collateralMint, + 100000, + ); // valued as $0.004 maint collateral + await mangoAccount.reload(client, group); + + await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5); + + try { + await client.perpPlaceOrder( + group, + fundingAccount, + 'MNGO-PERP', + Side.ask, + 40, + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4200, + PerpOrderType.limit, + 0, + 5, + ); + + await client.perpPlaceOrder( + group, + mangoAccount, + 'MNGO-PERP', + Side.bid, + 40, + 0.0011, // ui base quantity, 11 base lots, $0.044 + 0.044, // ui quote quantity + 4200, + PerpOrderType.market, + 0, + 5, + ); + + await client.perpConsumeAllEvents(group, 'MNGO-PERP'); + } finally { + await client.stubOracleSet(group, collateralOracle, PRICES['SOL']); + } + } + process.exit(); } diff --git a/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts b/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts index d4907f1c1..5004bd356 100644 --- a/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts +++ b/ts/client/src/scripts/mb-liqtest-settle-and-close-all.ts @@ -1,7 +1,8 @@ -import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { BN, AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; import { MangoClient } from '../client'; +import { Side, PerpOrderType } from '../accounts/perp'; import { MANGO_V4_ID } from '../constants'; // @@ -57,63 +58,18 @@ async function main() { await client.serum3SettleFunds(group, account, serumExternal); await client.serum3CloseOpenOrders(group, account, serumExternal); } - } - accounts = await client.getMangoAccountsForOwner(group, admin.publicKey); - for (let account of accounts) { - console.log(`settling borrows on account: ${account}`); - - // first, settle all borrows - for (let token of account.tokensActive()) { - const bank = group.getFirstBankByTokenIndex(token.tokenIndex); - const amount = token.balance(bank).toNumber(); - if (amount < 0) { - try { - await client.tokenDepositNative( - group, - account, - bank.mint, - Math.ceil(-amount), - ); - await account.reload(client, group); - } catch (error) { - console.log( - `failed to deposit ${bank.name} into ${account.publicKey}: ${error}`, - ); - process.exit(); - } - } + for (let perpPosition of account.perpActive()) { + const perpMarket = group.findPerpMarket(perpPosition.marketIndex)!; + console.log( + `closing perp orders on: ${account} for market ${perpMarket.name}`, + ); + await client.perpCancelAllOrders(group, account, perpMarket.name, 10); } } accounts = await client.getMangoAccountsForOwner(group, admin.publicKey); for (let account of accounts) { - console.log(`withdrawing deposits of account: ${account}`); - - // withdraw all funds - for (let token of account.tokensActive()) { - const bank = group.getFirstBankByTokenIndex(token.tokenIndex); - const amount = token.balance(bank).toNumber(); - if (amount > 0) { - try { - const allowBorrow = false; - await client.tokenWithdrawNative( - group, - account, - bank.mint, - amount, - allowBorrow, - ); - await account.reload(client, group); - } catch (error) { - console.log( - `failed to withdraw ${bank.name} from ${account.publicKey}: ${error}`, - ); - process.exit(); - } - } - } - // close account try { console.log(`closing account: ${account}`);