From 86c933164718c63c7a98e21ac13024b6b5779ec4 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:24:40 +0100 Subject: [PATCH] reduce only order type and mode for tokens and perp markets (#353) * reduce only order type and mode for tokens and perp markets Signed-off-by: microwavedcola1 * Fix from review Signed-off-by: microwavedcola1 * Fix from review Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 * tests Signed-off-by: microwavedcola1 * remove token Signed-off-by: microwavedcola1 * fixes Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- anchor | 2 +- cli/src/main.rs | 2 +- client/src/client.rs | 8 +- keeper/src/taker.rs | 2 +- programs/mango-v4/src/error.rs | 4 + .../mango-v4/src/instructions/flash_loan.rs | 20 +- programs/mango-v4/src/instructions/mod.rs | 2 + .../src/instructions/perp_create_market.rs | 3 +- .../src/instructions/perp_edit_market.rs | 5 + .../src/instructions/perp_place_order.rs | 34 +- .../src/instructions/serum3_edit_market.rs | 30 + .../src/instructions/serum3_place_order.rs | 4 + .../instructions/serum3_register_market.rs | 9 +- .../src/instructions/token_deposit.rs | 41 +- .../mango-v4/src/instructions/token_edit.rs | 5 + .../src/instructions/token_register.rs | 3 +- .../instructions/token_register_trustless.rs | 15 +- .../src/instructions/token_withdraw.rs | 3 + programs/mango-v4/src/lib.rs | 18 +- programs/mango-v4/src/state/bank.rs | 14 +- .../src/state/mango_account_components.rs | 5 + programs/mango-v4/src/state/perp_market.rs | 20 +- programs/mango-v4/src/state/serum3_market.rs | 9 +- .../tests/program_test/mango_client.rs | 149 ++++- .../tests/program_test/mango_setup.rs | 1 + .../mango-v4/tests/test_bankrupt_tokens.rs | 7 + programs/mango-v4/tests/test_basic.rs | 1 + .../mango-v4/tests/test_health_compute.rs | 3 + programs/mango-v4/tests/test_health_region.rs | 2 + programs/mango-v4/tests/test_liq_perps.rs | 5 + programs/mango-v4/tests/test_liq_tokens.rs | 3 + programs/mango-v4/tests/test_margin_trade.rs | 1 + programs/mango-v4/tests/test_perp.rs | 13 + programs/mango-v4/tests/test_perp_settle.rs | 6 + .../mango-v4/tests/test_perp_settle_fees.rs | 6 + .../mango-v4/tests/test_position_lifetime.rs | 5 + programs/mango-v4/tests/test_reduce_only.rs | 610 ++++++++++++++++++ ts/client/src/client.ts | 9 +- ts/client/src/mango_v4.ts | 162 ++++- ts/client/src/scripts/mb-admin.ts | 27 +- ts/client/src/scripts/mb-edit-perp-market.ts | 1 + .../src/scripts/mb-liqtest-make-candidates.ts | 4 + .../mb-liqtest-settle-and-close-all.ts | 4 +- 43 files changed, 1216 insertions(+), 61 deletions(-) create mode 100644 programs/mango-v4/src/instructions/serum3_edit_market.rs create mode 100644 programs/mango-v4/tests/test_reduce_only.rs diff --git a/anchor b/anchor index b3707b1fa..309c2c2f4 160000 --- a/anchor +++ b/anchor @@ -1 +1 @@ -Subproject commit b3707b1faaf6816cb3dd600074c81a39d373e952 +Subproject commit 309c2c2f4cce7c0a13d307fab3c7e2985bff3fa5 diff --git a/cli/src/main.rs b/cli/src/main.rs index d52488d6d..380e26802 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -162,7 +162,7 @@ async fn main() -> Result<(), anyhow::Error> { let owner = client::keypair_from_cli(&cmd.owner); let mint = client::pubkey_from_cli(&cmd.mint); let client = MangoClient::new_for_existing_account(client, account, owner).await?; - let txsig = client.token_deposit(mint, cmd.amount).await?; + let txsig = client.token_deposit(mint, cmd.amount, false).await?; println!("{}", txsig); } Command::JupiterSwap(cmd) => { diff --git a/client/src/client.rs b/client/src/client.rs index ca5606c2b..e65277bc3 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -302,7 +302,12 @@ impl MangoClient { ) } - pub async fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result { + pub async fn token_deposit( + &self, + mint: Pubkey, + amount: u64, + reduce_only: bool, + ) -> anyhow::Result { let token = self.context.token_by_mint(&mint)?; let token_index = token.token_index; let mint_info = token.mint_info; @@ -333,6 +338,7 @@ impl MangoClient { }, data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenDeposit { amount, + reduce_only, }), }; self.send_and_confirm_owner_tx(vec![ix]).await diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs index c9806ac5e..eaf24bf6d 100644 --- a/keeper/src/taker.rs +++ b/keeper/src/taker.rs @@ -117,7 +117,7 @@ async fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::E log::info!("Depositing {} {}", deposit_native, bank.name()); mango_client - .token_deposit(bank.mint, desired_balance.to_num()) + .token_deposit(bank.mint, desired_balance.to_num(), false) .await?; } diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index bfc5b8ae5..30fb55210 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -65,6 +65,10 @@ pub enum MangoError { TokenPositionDoesNotExist, #[msg("token deposits into accounts that are being liquidated must bring their health above the init threshold")] DepositsIntoLiquidatingMustRecover, + #[msg("token is in reduce only mode")] + TokenInReduceOnlyMode, + #[msg("market is in reduce only mode")] + MarketInReduceOnlyMode, } impl MangoError { diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 5f8d1a56f..9701cf40f 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -376,8 +376,10 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( let mut token_loan_details = Vec::with_capacity(changes.len()); for (change, oracle_price) in changes.iter().zip(oracle_prices.iter()) { let mut bank = health_ais[change.bank_index].load_mut::()?; + let position = account.token_position_mut_by_raw_index(change.raw_token_index); let native = position.native(&bank); + let approved_amount = I80F48::from(bank.flash_loan_approved_amount); let loan = if native.is_positive() { @@ -386,8 +388,21 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( approved_amount }; + let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate); + cm!(bank.collected_fees_native += loan_origination_fee); + + let change_amount = cm!(change.amount - loan_origination_fee); + let native_after_change = cm!(native + change_amount); + if bank.is_reduce_only() { + require!( + (change_amount < 0 && native_after_change >= 0) + || (change_amount > 0 && native_after_change < 1), + MangoError::TokenInReduceOnlyMode + ); + } + // Enforce min vault to deposits ratio - if loan > 0 { + if native_after_change < 0 { let vault_ai = vaults .iter() .find(|vault_ai| vault_ai.key == &bank.vault) @@ -395,9 +410,6 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( bank.enforce_min_vault_to_deposits_ratio(vault_ai)?; } - let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate); - cm!(bank.collected_fees_native += loan_origination_fee); - let is_active = bank.change_without_fee( position, cm!(change.amount - loan_origination_fee), diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index a62bd0d73..ede7669da 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -32,6 +32,7 @@ pub use serum3_cancel_order::*; pub use serum3_close_open_orders::*; pub use serum3_create_open_orders::*; pub use serum3_deregister_market::*; +pub use serum3_edit_market::*; pub use serum3_liq_force_cancel_orders::*; pub use serum3_place_order::*; pub use serum3_register_market::*; @@ -84,6 +85,7 @@ mod serum3_cancel_order; mod serum3_close_open_orders; mod serum3_create_open_orders; mod serum3_deregister_market; +mod serum3_edit_market; mod serum3_liq_force_cancel_orders; mod serum3_place_order; mod serum3_register_market; diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index a68126c89..7573c403e 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -130,7 +130,8 @@ pub fn perp_create_market( settle_pnl_limit_factor, padding3: Default::default(), settle_pnl_limit_window_size_ts, - reserved: [0; 1944], + reduce_only: 0, + reserved: [0; 1943], }; let oracle_price = diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index dc5d6bac2..3dbafc981 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -49,6 +49,7 @@ pub fn perp_edit_market( stable_price_growth_limit_opt: Option, settle_pnl_limit_factor_opt: Option, settle_pnl_limit_window_size_ts_opt: Option, + reduce_only_opt: Option, ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_mut()?; @@ -171,6 +172,10 @@ pub fn perp_edit_market( perp_market.settle_pnl_limit_window_size_ts = settle_pnl_limit_window_size_ts; } + if let Some(reduce_only) = reduce_only_opt { + perp_market.reduce_only = if reduce_only { 1 } else { 0 }; + }; + emit!(PerpMarketMetaDataLog { mango_group: ctx.accounts.group.key(), perp_market: ctx.accounts.perp_market.key(), diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index 06794b872..cd66742af 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -3,6 +3,7 @@ use anchor_lang::prelude::*; use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::{new_fixed_order_account_retriever, new_health_cache}; +use crate::state::Side; use crate::state::{ BookSide, EventQueue, Group, MangoAccountFixed, MangoAccountLoader, Order, Orderbook, PerpMarket, @@ -38,7 +39,7 @@ pub struct PerpPlaceOrder<'info> { // TODO #[allow(clippy::too_many_arguments)] -pub fn perp_place_order(ctx: Context, order: Order, limit: u8) -> Result<()> { +pub fn perp_place_order(ctx: Context, mut order: Order, limit: u8) -> Result<()> { require_gte!(order.max_base_lots, 0); require_gte!(order.max_quote_lots, 0); @@ -109,8 +110,35 @@ pub fn perp_place_order(ctx: Context, order: Order, limit: u8) - let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); - // TODO apply reduce_only flag to compute final base_lots, also process event queue - require!(order.reduce_only == false, MangoError::SomeError); + let pp = account.perp_position(perp_market_index)?; + let effective_pos = pp.effective_base_position_lots(); + let max_base_lots = if order.reduce_only || perp_market.is_reduce_only() { + if (order.side == Side::Bid && effective_pos >= 0) + || (order.side == Side::Ask && effective_pos <= 0) + { + 0 + } else if order.side == Side::Bid { + // ignores open asks + (effective_pos + pp.bids_base_lots) + .min(0) + .abs() + .min(order.max_base_lots) + } else { + // ignores open bids + (effective_pos - pp.asks_base_lots) + .max(0) + .min(order.max_base_lots) + } + } else { + order.max_base_lots + }; + if perp_market.is_reduce_only() { + require!( + order.reduce_only || max_base_lots == order.max_base_lots, + MangoError::MarketInReduceOnlyMode + ) + }; + order.max_base_lots = max_base_lots; book.new_order( order, diff --git a/programs/mango-v4/src/instructions/serum3_edit_market.rs b/programs/mango-v4/src/instructions/serum3_edit_market.rs new file mode 100644 index 000000000..fed566f34 --- /dev/null +++ b/programs/mango-v4/src/instructions/serum3_edit_market.rs @@ -0,0 +1,30 @@ +use anchor_lang::prelude::*; + +use crate::state::*; + +#[derive(Accounts)] +#[instruction(market_index: Serum3MarketIndex)] +pub struct Serum3EditMarket<'info> { + #[account( + has_one = admin, + constraint = group.load()?.serum3_supported() + )] + pub group: AccountLoader<'info, Group>, + pub admin: Signer<'info>, + + #[account( + mut, + has_one = group + )] + pub market: AccountLoader<'info, Serum3Market>, +} + +pub fn serum3_edit_market( + ctx: Context, + reduce_only_opt: Option, +) -> Result<()> { + if let Some(reduce_only) = reduce_only_opt { + ctx.accounts.market.load_mut()?.reduce_only = u8::from(reduce_only); + }; + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 6a04fda64..503508886 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -212,6 +212,10 @@ pub fn serum3_place_order( limit: u16, ) -> Result<()> { let serum_market = ctx.accounts.serum_market.load()?; + require!( + !serum_market.is_reduce_only(), + MangoError::MarketInReduceOnlyMode + ); // // Validation diff --git a/programs/mango-v4/src/instructions/serum3_register_market.rs b/programs/mango-v4/src/instructions/serum3_register_market.rs index ab1e34b43..36dc790ce 100644 --- a/programs/mango-v4/src/instructions/serum3_register_market.rs +++ b/programs/mango-v4/src/instructions/serum3_register_market.rs @@ -78,15 +78,16 @@ pub fn serum3_register_market( let mut serum_market = ctx.accounts.serum_market.load_init()?; *serum_market = Serum3Market { - name: fill_from_str(&name)?, group: ctx.accounts.group.key(), + base_token_index: base_bank.token_index, + quote_token_index: quote_bank.token_index, + reduce_only: 0, + padding1: Default::default(), + name: fill_from_str(&name)?, serum_program: ctx.accounts.serum_program.key(), serum_market_external: ctx.accounts.serum_market_external.key(), market_index, - base_token_index: base_bank.token_index, - quote_token_index: quote_bank.token_index, bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?, - padding1: Default::default(), padding2: Default::default(), registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(), reserved: [0; 128], diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 9ee258dfc..ce035600c 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -100,20 +100,44 @@ impl<'a, 'info> DepositCommon<'a, 'info> { &self, remaining_accounts: &[AccountInfo], amount: u64, + reduce_only: bool, allow_token_account_closure: bool, ) -> Result<()> { require_msg!(amount > 0, "deposit amount must be positive"); - let token_index = self.bank.load()?.token_index; + let mut bank = self.bank.load_mut()?; + let token_index = bank.token_index; + + let amount_i80f48 = { + // Get the account's position for that token index + let account = self.account.load_full()?; + let position = account.token_position(token_index)?; + + let amount_i80f48 = if reduce_only || bank.is_reduce_only() { + position + .native(&bank) + .min(I80F48::ZERO) + .abs() + .ceil() + .min(I80F48::from(amount)) + } else { + I80F48::from(amount) + }; + if bank.is_reduce_only() { + require!( + reduce_only || amount_i80f48 == I80F48::from(amount), + MangoError::TokenInReduceOnlyMode + ); + } + amount_i80f48 + }; // Get the account's position for that token index let mut account = self.account.load_full_mut()?; let (position, raw_token_index) = account.token_position_mut(token_index)?; - let amount_i80f48 = I80F48::from(amount); let position_is_active = { - let mut bank = self.bank.load_mut()?; bank.deposit( position, amount_i80f48, @@ -122,10 +146,9 @@ impl<'a, 'info> DepositCommon<'a, 'info> { }; // Transfer the actual tokens - token::transfer(self.transfer_ctx(), amount)?; + token::transfer(self.transfer_ctx(), amount_i80f48.to_num::())?; let indexed_position = position.indexed_position; - let bank = self.bank.load()?; let oracle_price = bank.oracle_price( &AccountInfoRef::borrow(self.oracle.as_ref())?, None, // staleness checked in health @@ -143,6 +166,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> { deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), }); + drop(bank); // // Health computation @@ -187,7 +211,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> { } } -pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { +pub fn token_deposit(ctx: Context, amount: u64, reduce_only: bool) -> Result<()> { { let token_index = ctx.accounts.bank.load()?.token_index; let mut account = ctx.accounts.account.load_full_mut()?; @@ -204,12 +228,13 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { token_authority: &ctx.accounts.token_authority, token_program: &ctx.accounts.token_program, } - .deposit_into_existing(ctx.remaining_accounts, amount, true) + .deposit_into_existing(ctx.remaining_accounts, amount, reduce_only, true) } pub fn token_deposit_into_existing( ctx: Context, amount: u64, + reduce_only: bool, ) -> Result<()> { DepositCommon { group: &ctx.accounts.group, @@ -221,5 +246,5 @@ pub fn token_deposit_into_existing( token_authority: &ctx.accounts.token_authority, token_program: &ctx.accounts.token_program, } - .deposit_into_existing(ctx.remaining_accounts, amount, false) + .deposit_into_existing(ctx.remaining_accounts, amount, reduce_only, false) } diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index bc485ee78..74afdeef3 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -56,6 +56,7 @@ pub fn token_edit( deposit_weight_scale_start_quote_opt: Option, reset_stable_price: bool, reset_net_borrow_limit: bool, + reduce_only_opt: Option, ) -> Result<()> { let mut mint_info = ctx.accounts.mint_info.load_mut()?; mint_info.verify_banks_ais(ctx.remaining_accounts)?; @@ -170,6 +171,10 @@ pub fn token_edit( bank.deposit_weight_scale_start_quote = deposit_weight_scale_start_quote; } + if let Some(reduce_only) = reduce_only_opt { + bank.reduce_only = if reduce_only { 1 } else { 0 }; + }; + // unchanged - // dust // flash_loan_token_account_initial diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 36d63b0c0..4a0a7aade 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -150,7 +150,8 @@ pub fn token_register( net_borrows_in_window: 0, borrow_weight_scale_start_quote: f64::MAX, deposit_weight_scale_start_quote: f64::MAX, - reserved: [0; 2120], + reduce_only: 0, + reserved: [0; 2119], }; require_gt!(bank.max_rate, MINIMUM_MAX_RATE); diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index 2bba06c5f..85707e364 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -82,6 +82,12 @@ pub fn token_register_trustless( mint: ctx.accounts.mint.key(), vault: ctx.accounts.vault.key(), oracle: ctx.accounts.oracle.key(), + oracle_config: OracleConfig { + conf_filter: I80F48::from_num(0.10), + max_staleness_slots: -1, + reserved: [0; 72], + }, + stable_price_model: StablePriceModel::default(), deposit_index: INDEX_START, borrow_index: INDEX_START, indexed_deposits: I80F48::ZERO, @@ -111,12 +117,6 @@ pub fn token_register_trustless( bump: *ctx.bumps.get("bank").ok_or(MangoError::SomeError)?, mint_decimals: ctx.accounts.mint.decimals, bank_num: 0, - oracle_config: OracleConfig { - conf_filter: I80F48::from_num(0.10), - max_staleness_slots: -1, - reserved: [0; 72], - }, - stable_price_model: StablePriceModel::default(), min_vault_to_deposits_ratio: 0.2, net_borrow_limit_window_size_ts, last_net_borrows_window_start_ts: now_ts / net_borrow_limit_window_size_ts @@ -125,7 +125,8 @@ pub fn token_register_trustless( net_borrows_in_window: 0, borrow_weight_scale_start_quote: f64::MAX, deposit_weight_scale_start_quote: f64::MAX, - reserved: [0; 2120], + reduce_only: 0, + reserved: [0; 2119], }; require_gt!(bank.max_rate, MINIMUM_MAX_RATE); diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 8750d95ba..9dfbc6319 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -96,6 +96,9 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let is_borrow = amount > native_position; require!(allow_borrow || !is_borrow, MangoError::SomeError); + if bank.is_reduce_only() { + require!(!is_borrow, MangoError::TokenInReduceOnlyMode); + } let amount_i80f48 = I80F48::from(amount); diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 436ccd532..1a359c5cd 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -130,6 +130,7 @@ pub mod mango_v4 { deposit_weight_scale_start_quote_opt: Option, reset_stable_price: bool, reset_net_borrow_limit: bool, + reduce_only_opt: Option, ) -> Result<()> { instructions::token_edit( ctx, @@ -154,6 +155,7 @@ pub mod mango_v4 { deposit_weight_scale_start_quote_opt, reset_stable_price, reset_net_borrow_limit, + reduce_only_opt, ) } @@ -235,15 +237,16 @@ pub mod mango_v4 { instructions::stub_oracle_set(ctx, price) } - pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { - instructions::token_deposit(ctx, amount) + pub fn token_deposit(ctx: Context, amount: u64, reduce_only: bool) -> Result<()> { + instructions::token_deposit(ctx, amount, reduce_only) } pub fn token_deposit_into_existing( ctx: Context, amount: u64, + reduce_only: bool, ) -> Result<()> { - instructions::token_deposit_into_existing(ctx, amount) + instructions::token_deposit_into_existing(ctx, amount, reduce_only) } pub fn token_withdraw( @@ -292,6 +295,13 @@ pub mod mango_v4 { instructions::serum3_register_market(ctx, market_index, name) } + pub fn serum3_edit_market( + ctx: Context, + reduce_only_opt: Option, + ) -> Result<()> { + instructions::serum3_edit_market(ctx, reduce_only_opt) + } + // note: // pub fn serum3_edit_market - doesn't exist since a mango serum3 market only contains the properties // registered base and quote token pairs, and serum3 external market its pointing to, and none of them @@ -491,6 +501,7 @@ pub mod mango_v4 { stable_price_growth_limit_opt: Option, settle_pnl_limit_factor_opt: Option, settle_pnl_limit_window_size_ts: Option, + reduce_only_opt: Option, ) -> Result<()> { instructions::perp_edit_market( ctx, @@ -518,6 +529,7 @@ pub mod mango_v4 { stable_price_growth_limit_opt, settle_pnl_limit_factor_opt, settle_pnl_limit_window_size_ts, + reduce_only_opt, ) } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 3eeb9bc06..91a74186b 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -127,8 +127,10 @@ pub struct Bank { /// See scaled_init_asset_weight(). pub deposit_weight_scale_start_quote: f64, + pub reduce_only: u8, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 2120], + pub reserved: [u8; 2119], } const_assert_eq!( size_of::(), @@ -155,7 +157,8 @@ const_assert_eq!( + 8 * 4 + 8 + 8 - + 2120 + + 1 + + 2119 ); const_assert_eq!(size_of::(), 3064); const_assert_eq!(size_of::() % 8, 0); @@ -215,7 +218,8 @@ impl Bank { net_borrows_in_window: 0, borrow_weight_scale_start_quote: f64::MAX, deposit_weight_scale_start_quote: f64::MAX, - reserved: [0; 2120], + reduce_only: 0, + reserved: [0; 2119], } } @@ -225,6 +229,10 @@ impl Bank { .trim_matches(char::from(0)) } + pub fn is_reduce_only(&self) -> bool { + self.reduce_only == 1 + } + #[inline(always)] pub fn native_borrows(&self) -> I80F48 { cm!(self.borrow_index * self.indexed_borrows) diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 0a7024d9b..b089d60cd 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -284,6 +284,11 @@ impl PerpPosition { self.base_position_lots } + // This takes into account base lots from unprocessed events, but not anything from open orders + pub fn effective_base_position_lots(&self) -> i64 { + self.base_position_lots + self.taker_base_lots + } + pub fn quote_position_native(&self) -> I80F48 { self.quote_position_native } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 785f7865f..62c48ea6c 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -105,7 +105,9 @@ pub struct PerpMarket { /// Window size in seconds for the perp settlement limit pub settle_pnl_limit_window_size_ts: u64, - pub reserved: [u8; 1944], + pub reduce_only: u8, + + pub reserved: [u8; 1943], } const_assert_eq!( @@ -138,7 +140,8 @@ const_assert_eq!( + 4 * 3 + 8 + 8 - + 1944 + + 1 + + 1943 ); const_assert_eq!(size_of::(), 2808); const_assert_eq!(size_of::() % 8, 0); @@ -150,6 +153,10 @@ impl PerpMarket { .trim_matches(char::from(0)) } + pub fn is_reduce_only(&self) -> bool { + self.reduce_only == 1 + } + pub fn elligible_for_group_insurance_fund(&self) -> bool { self.group_insurance_fund == 1 } @@ -306,6 +313,8 @@ impl PerpMarket { perp_market_index: 0, trusted_market: 0, group_insurance_fund: 0, + bump: 0, + base_decimals: 0, name: Default::default(), bids: Pubkey::new_unique(), asks: Pubkey::new_unique(), @@ -325,8 +334,6 @@ impl PerpMarket { init_liab_weight: I80F48::from(1), open_interest: 0, seq_num: 0, - bump: 0, - base_decimals: 0, registration_time: 0, min_funding: I80F48::ZERO, max_funding: I80F48::ZERO, @@ -343,10 +350,11 @@ impl PerpMarket { settle_fee_flat: 0.0, settle_fee_amount_threshold: 0.0, settle_fee_fraction_low_health: 0.0, - padding3: Default::default(), settle_pnl_limit_factor: 0.2, + padding3: Default::default(), settle_pnl_limit_window_size_ts: 24 * 60 * 60, - reserved: [0; 1944], + reduce_only: 0, + reserved: [0; 1943], } } } diff --git a/programs/mango-v4/src/state/serum3_market.rs b/programs/mango-v4/src/state/serum3_market.rs index 623eb171a..a67d46a83 100644 --- a/programs/mango-v4/src/state/serum3_market.rs +++ b/programs/mango-v4/src/state/serum3_market.rs @@ -15,7 +15,8 @@ pub struct Serum3Market { pub base_token_index: TokenIndex, // ABI: Clients rely on this being at offset 42 pub quote_token_index: TokenIndex, - pub padding1: [u8; 4], + pub reduce_only: u8, + pub padding1: [u8; 3], pub name: [u8; 16], pub serum_program: Pubkey, pub serum_market_external: Pubkey, @@ -32,7 +33,7 @@ pub struct Serum3Market { } const_assert_eq!( size_of::(), - 32 + 2 + 2 + 4 + 16 + 2 * 32 + 2 + 1 + 5 + 8 + 128 + 32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 5 + 8 + 128 ); const_assert_eq!(size_of::(), 264); const_assert_eq!(size_of::() % 8, 0); @@ -43,6 +44,10 @@ impl Serum3Market { .unwrap() .trim_matches(char::from(0)) } + + pub fn is_reduce_only(&self) -> bool { + self.reduce_only == 1 + } } #[account(zero_copy)] diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 923199d03..26fd15f41 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -617,7 +617,7 @@ impl ClientInstruction for TokenWithdrawInstruction { pub struct TokenDepositInstruction { pub amount: u64, - + pub reduce_only: bool, pub account: Pubkey, pub owner: TestKeypair, pub token_account: Pubkey, @@ -635,6 +635,7 @@ impl ClientInstruction for TokenDepositInstruction { let program_id = mango_v4::id(); let instruction = Self::Instruction { amount: self.amount, + reduce_only: self.reduce_only, }; // load account so we know its mint @@ -688,7 +689,7 @@ impl ClientInstruction for TokenDepositInstruction { pub struct TokenDepositIntoExistingInstruction { pub amount: u64, - + pub reduce_only: bool, pub account: Pubkey, pub token_account: Pubkey, pub token_authority: TestKeypair, @@ -705,6 +706,7 @@ impl ClientInstruction for TokenDepositIntoExistingInstruction { let program_id = mango_v4::id(); let instruction = Self::Instruction { amount: self.amount, + reduce_only: self.reduce_only, }; // load account so we know its mint @@ -1083,6 +1085,7 @@ impl ClientInstruction for TokenResetStablePriceModel { deposit_weight_scale_start_quote_opt: None, reset_stable_price: true, reset_net_borrow_limit: false, + reduce_only_opt: None, }; let accounts = Self::Accounts { @@ -1160,6 +1163,82 @@ impl ClientInstruction for TokenResetNetBorrows { deposit_weight_scale_start_quote_opt: None, reset_stable_price: false, reset_net_borrow_limit: true, + reduce_only_opt: None, + }; + + let accounts = Self::Accounts { + group: self.group, + admin: self.admin.pubkey(), + mint_info: mint_info_key, + oracle: mint_info.oracle, + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + instruction + .accounts + .extend(mint_info.banks().iter().map(|&k| AccountMeta { + pubkey: k, + is_signer: false, + is_writable: true, + })); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.admin] + } +} + +pub struct TokenMakeReduceOnly { + pub group: Pubkey, + pub admin: TestKeypair, + pub mint: Pubkey, +} + +#[async_trait::async_trait(?Send)] +impl ClientInstruction for TokenMakeReduceOnly { + type Accounts = mango_v4::accounts::TokenEdit; + type Instruction = mango_v4::instruction::TokenEdit; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + + let mint_info_key = Pubkey::find_program_address( + &[ + b"MintInfo".as_ref(), + self.group.as_ref(), + self.mint.as_ref(), + ], + &program_id, + ) + .0; + let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap(); + + let instruction = Self::Instruction { + oracle_opt: None, + oracle_config_opt: None, + group_insurance_fund_opt: None, + interest_rate_params_opt: None, + loan_fee_rate_opt: None, + loan_origination_fee_rate_opt: None, + maint_asset_weight_opt: None, + init_asset_weight_opt: None, + maint_liab_weight_opt: None, + init_liab_weight_opt: None, + liquidation_fee_opt: None, + stable_price_delay_interval_seconds_opt: None, + stable_price_delay_growth_limit_opt: None, + stable_price_growth_limit_opt: None, + min_vault_to_deposits_ratio_opt: None, + net_borrow_limit_per_window_quote_opt: None, + net_borrow_limit_window_size_ts_opt: None, + borrow_weight_scale_start_quote_opt: None, + deposit_weight_scale_start_quote_opt: None, + reset_stable_price: false, + reset_net_borrow_limit: false, + reduce_only_opt: Some(true), }; let accounts = Self::Accounts { @@ -2570,6 +2649,69 @@ impl ClientInstruction for PerpResetStablePriceModel { stable_price_growth_limit_opt: None, settle_pnl_limit_factor_opt: None, settle_pnl_limit_window_size_ts: None, + reduce_only_opt: None, + }; + + let accounts = Self::Accounts { + group: self.group, + admin: self.admin.pubkey(), + perp_market: self.perp_market, + oracle: perp_market.oracle, + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.admin] + } +} + +pub struct PerpMakeReduceOnly { + pub group: Pubkey, + pub admin: TestKeypair, + pub perp_market: Pubkey, +} + +#[async_trait::async_trait(?Send)] +impl ClientInstruction for PerpMakeReduceOnly { + type Accounts = mango_v4::accounts::PerpEditMarket; + type Instruction = mango_v4::instruction::PerpEditMarket; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + + let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); + + let instruction = Self::Instruction { + oracle_opt: None, + oracle_config_opt: None, + base_decimals_opt: None, + maint_asset_weight_opt: None, + init_asset_weight_opt: None, + maint_liab_weight_opt: None, + init_liab_weight_opt: None, + liquidation_fee_opt: None, + maker_fee_opt: None, + taker_fee_opt: None, + min_funding_opt: None, + max_funding_opt: None, + impact_quantity_opt: None, + group_insurance_fund_opt: None, + trusted_market_opt: None, + fee_penalty_opt: None, + settle_fee_flat_opt: None, + settle_fee_amount_threshold_opt: None, + settle_fee_fraction_low_health_opt: None, + stable_price_delay_interval_seconds_opt: None, + stable_price_delay_growth_limit_opt: None, + stable_price_growth_limit_opt: None, + settle_pnl_limit_factor_opt: None, + settle_pnl_limit_window_size_ts: None, + reduce_only_opt: Some(true), }; let accounts = Self::Accounts { @@ -2666,6 +2808,7 @@ pub struct PerpPlaceOrderInstruction { pub price_lots: i64, pub max_base_lots: i64, pub max_quote_lots: i64, + pub reduce_only: bool, pub client_order_id: u64, } #[async_trait::async_trait(?Send)] @@ -2684,7 +2827,7 @@ impl ClientInstruction for PerpPlaceOrderInstruction { max_quote_lots: self.max_quote_lots, client_order_id: self.client_order_id, order_type: PlaceOrderType::Limit, - reduce_only: false, + reduce_only: self.reduce_only, expiry_timestamp: 0, limit: 10, }; diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index f2e753762..837d3e247 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -179,6 +179,7 @@ pub async fn create_funded_account( solana, TokenDepositInstruction { amount: amounts, + reduce_only: false, account, owner, token_account: payer.token_accounts[mint.index], diff --git a/programs/mango-v4/tests/test_bankrupt_tokens.rs b/programs/mango-v4/tests/test_bankrupt_tokens.rs index dcb2c3a2e..26db180f7 100644 --- a/programs/mango-v4/tests/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/test_bankrupt_tokens.rs @@ -61,6 +61,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 20, + reduce_only: false, account: vault_account, owner, token_account: payer_mint_accounts[0], @@ -97,6 +98,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit1_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[2], @@ -110,6 +112,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit2_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[3], @@ -350,6 +353,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: vault_amount, + reduce_only: false, account: vault_account, owner, token_account, @@ -367,6 +371,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 20, + reduce_only: false, account: vault_account, owner, token_account: payer_mint_accounts[0], @@ -403,6 +408,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit1_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[2], @@ -416,6 +422,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit2_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[3], diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 0db9978f1..2c59bc8de 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -105,6 +105,7 @@ async fn test_basic() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit_amount, + reduce_only: false, account, owner, token_account: payer_mint0_account, diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index 5bae1523c..dff6d7cce 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -146,6 +146,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 10, + reduce_only: false, account, owner, token_account: payer_mint_accounts[0], @@ -252,6 +253,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -262,6 +264,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 10, + reduce_only: false, account, owner, token_account: payer_mint_accounts[0], diff --git a/programs/mango-v4/tests/test_health_region.rs b/programs/mango-v4/tests/test_health_region.rs index 8f7853eff..f45408ae6 100644 --- a/programs/mango-v4/tests/test_health_region.rs +++ b/programs/mango-v4/tests/test_health_region.rs @@ -64,6 +64,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 1, + reduce_only: false, account, owner, token_account: payer_mint_accounts[0], @@ -91,6 +92,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { .await; tx.add_instruction(TokenDepositInstruction { amount: repay_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[1], diff --git a/programs/mango-v4/tests/test_liq_perps.rs b/programs/mango-v4/tests/test_liq_perps.rs index e0bf8d858..78631dae6 100644 --- a/programs/mango-v4/tests/test_liq_perps.rs +++ b/programs/mango-v4/tests/test_liq_perps.rs @@ -95,6 +95,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 1, + reduce_only: false, account, owner, token_account: payer_mint_accounts[1], @@ -119,6 +120,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> { // health was 1000 * 0.6 = 600; this order is -14*100*(1.4-1) = -560 max_base_lots: 14, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -318,6 +320,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr price_lots, max_base_lots: 20, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -333,6 +336,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr price_lots, max_base_lots: 20, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -610,6 +614,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr solana, TokenDepositInstruction { amount: 1, + reduce_only: false, account: account_1, owner, token_account: payer_mint_accounts[1], diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index ab24be1f5..f7309e590 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -223,6 +223,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: 100000, + reduce_only: false, account: vault_account, owner, token_account, @@ -260,6 +261,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit1_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[2], @@ -273,6 +275,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit2_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[3], diff --git a/programs/mango-v4/tests/test_margin_trade.rs b/programs/mango-v4/tests/test_margin_trade.rs index 88d5fa104..d51a123f2 100644 --- a/programs/mango-v4/tests/test_margin_trade.rs +++ b/programs/mango-v4/tests/test_margin_trade.rs @@ -88,6 +88,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { solana, TokenDepositInstruction { amount: deposit_amount_initial, + reduce_only: false, account, owner, token_account: payer_mint0_account, diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index 4c0f18596..1386c504d 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -109,6 +109,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -150,6 +151,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 1, }, ) @@ -184,6 +186,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 2, }, ) @@ -217,6 +220,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 4, }, ) @@ -250,6 +254,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 5, }, ) @@ -267,6 +272,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 6, }, ) @@ -328,6 +334,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 7, }, ) @@ -345,6 +352,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 8, }, ) @@ -609,6 +617,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 6, }, ) @@ -691,6 +700,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 60, }, ) @@ -720,6 +730,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { price_lots, max_base_lots: 2, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 61, }, ) @@ -771,6 +782,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { price_lots: price_lots + 2, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 62, }, ) @@ -800,6 +812,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { price_lots: price_lots + 3, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 63, }, ) diff --git a/programs/mango-v4/tests/test_perp_settle.rs b/programs/mango-v4/tests/test_perp_settle.rs index 776365d46..3c4edd021 100644 --- a/programs/mango-v4/tests/test_perp_settle.rs +++ b/programs/mango-v4/tests/test_perp_settle.rs @@ -139,6 +139,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -155,6 +156,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -613,6 +615,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -629,6 +632,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -889,6 +893,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -905,6 +910,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) diff --git a/programs/mango-v4/tests/test_perp_settle_fees.rs b/programs/mango-v4/tests/test_perp_settle_fees.rs index 8cf38b8a8..d4589c5b8 100644 --- a/programs/mango-v4/tests/test_perp_settle_fees.rs +++ b/programs/mango-v4/tests/test_perp_settle_fees.rs @@ -79,6 +79,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit_amount, + reduce_only: false, account: account_0, owner, token_account: payer_mint_accounts[0], @@ -93,6 +94,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit_amount, + reduce_only: false, account: account_0, owner, token_account: payer_mint_accounts[1], @@ -111,6 +113,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit_amount, + reduce_only: false, account: account_1, owner, token_account: payer_mint_accounts[0], @@ -125,6 +128,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { solana, TokenDepositInstruction { amount: deposit_amount, + reduce_only: false, account: account_1, owner, token_account: payer_mint_accounts[1], @@ -214,6 +218,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) @@ -230,6 +235,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { price_lots, max_base_lots: 1, max_quote_lots: i64::MAX, + reduce_only: false, client_order_id: 0, }, ) diff --git a/programs/mango-v4/tests/test_position_lifetime.rs b/programs/mango-v4/tests/test_position_lifetime.rs index 8913dd725..2d8cd5b06 100644 --- a/programs/mango-v4/tests/test_position_lifetime.rs +++ b/programs/mango-v4/tests/test_position_lifetime.rs @@ -78,6 +78,7 @@ async fn test_position_lifetime() -> Result<()> { solana, TokenDepositIntoExistingInstruction { amount: deposit_amount, + reduce_only: false, account, token_account: payer_mint_accounts[0], token_authority: payer, @@ -93,6 +94,7 @@ async fn test_position_lifetime() -> Result<()> { solana, TokenDepositInstruction { amount: deposit_amount, + reduce_only: false, account, owner, token_account: payer_token, @@ -109,6 +111,7 @@ async fn test_position_lifetime() -> Result<()> { solana, TokenDepositIntoExistingInstruction { amount: deposit_amount, + reduce_only: false, account, token_account: payer_mint_accounts[0], token_authority: payer, @@ -160,6 +163,7 @@ async fn test_position_lifetime() -> Result<()> { solana, TokenDepositInstruction { amount: collateral_amount, + reduce_only: false, account, owner, token_account: payer_mint_accounts[0], @@ -197,6 +201,7 @@ async fn test_position_lifetime() -> Result<()> { TokenDepositInstruction { // deposit withdraw amount + some more to cover loan origination fees amount: borrow_amount + 2, + reduce_only: false, account, owner, token_account: payer_mint_accounts[1], diff --git a/programs/mango-v4/tests/test_reduce_only.rs b/programs/mango-v4/tests/test_reduce_only.rs new file mode 100644 index 000000000..cb4a539a3 --- /dev/null +++ b/programs/mango-v4/tests/test_reduce_only.rs @@ -0,0 +1,610 @@ +#![cfg(feature = "test-bpf")] + +use fixed::types::I80F48; +use mango_setup::*; +use mango_v4::state::{Bank, MangoAccount, PerpMarket, Side}; +use program_test::*; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_reduce_only_token() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..=2]; + let payer_mint_accounts = &context.users[1].token_accounts[0..=2]; + + let initial_token_deposit = 10_000; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + // + // SETUP: Prepare accounts + // + let account_0 = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[0..=2], + initial_token_deposit, + 0, + ) + .await; + + let account_1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[0..=1], + initial_token_deposit, + 0, + ) + .await; + + // make token reduce only + send_tx( + solana, + TokenMakeReduceOnly { + admin, + group, + mint: mints[0].pubkey, + }, + ) + .await + .unwrap(); + + // + // Withdraw deposits + // + + // deposit without reduce_only should fail + let res = send_tx( + solana, + TokenDepositInstruction { + amount: 10, + reduce_only: false, + account: account_0, + owner, + token_account: payer_mint_accounts[0], + token_authority: payer, + bank_index: 0, + }, + ) + .await; + assert!(res.is_err()); + + // deposit with reduce_only should pass with no effect + send_tx( + solana, + TokenDepositInstruction { + amount: 10, + reduce_only: true, + account: account_0, + owner, + token_account: payer_mint_accounts[0], + token_authority: payer, + bank_index: 0, + }, + ) + .await + .unwrap(); + let mango_account_0 = solana.get_account::(account_0).await; + let bank = solana.get_account::(tokens[0].bank).await; + let native = mango_account_0.tokens[0].native(&bank); + assert_eq!(native.to_num::(), initial_token_deposit); + + // withdraw all should pass + send_tx( + solana, + TokenWithdrawInstruction { + amount: initial_token_deposit, + allow_borrow: false, + account: account_0, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // borrowing should fail + let res = send_tx( + solana, + TokenWithdrawInstruction { + amount: 1, + allow_borrow: true, + account: account_0, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await; + assert!(res.is_err()); + + // + // Repay borrows + // + send_tx( + solana, + TokenWithdrawInstruction { + amount: initial_token_deposit / 2, + allow_borrow: true, + account: account_1, + owner, + token_account: payer_mint_accounts[2], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // make token reduce only + send_tx( + solana, + TokenMakeReduceOnly { + admin, + group, + mint: mints[2].pubkey, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + TokenDepositInstruction { + amount: initial_token_deposit, + reduce_only: true, + account: account_1, + owner, + token_account: payer_mint_accounts[2], + token_authority: payer, + bank_index: 0, + }, + ) + .await + .unwrap(); + + Ok(()) +} + +#[tokio::test] +async fn test_perp_reduce_only() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..=2]; + + let initial_token_deposit = 1000_000; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let account_0 = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[0..1], + initial_token_deposit, + 0, + ) + .await; + let account_1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[0..1], + initial_token_deposit * 100, // Fund 100x, so that this is not the bound for what account_0 can settle + 0, + ) + .await; + + // + // TEST: Create a perp market + // + let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx( + solana, + PerpCreateMarketInstruction { + group, + admin, + payer, + perp_market_index: 0, + quote_lot_size: 10, + base_lot_size: 100, + maint_asset_weight: 0.975, + init_asset_weight: 0.95, + maint_liab_weight: 1.025, + init_liab_weight: 1.05, + liquidation_fee: 0.012, + maker_fee: 0.0002, + taker_fee: 0.000, + settle_pnl_limit_factor: -1., + settle_pnl_limit_window_size_ts: 24 * 60 * 60, + ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await + }, + ) + .await + .unwrap(); + + let price_lots = { + let perp_market = solana.get_account::(perp_market).await; + perp_market.native_price_to_lot(I80F48::from(1000)) + }; + + // Set the initial oracle price + set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1000.0).await; + + // + // Place orders and create a position + // + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 2, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 2, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + + // account_0 - place a new bid + // when user has a long, and market is in reduce only, + // to reduce incoming asks to reduce position, we ignore existing bids + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots: price_lots / 2, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + // account_1 - place a new ask + // when user has a short, and market is in reduce only, + // to reduce incoming bids to reduce position, we ignore existing asks + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + + // + // Make market reduce only + // + send_tx( + solana, + PerpMakeReduceOnly { + group, + admin, + perp_market, + }, + ) + .await + .unwrap(); + + // account_0 - place a new bid with reduce only false should fail + let res = send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await; + assert!(res.is_err()); + + // account_0 - place a new bid with reduce only true should do nothing + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: true, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_0 = solana.get_account::(account_0).await; + assert_eq!(mango_account_0.perps[0].bids_base_lots, 1); + + // account_0 - place a new ask should pass + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_0 = solana.get_account::(account_0).await; + assert_eq!(mango_account_0.perps[0].asks_base_lots, 1); + + // account_0 - place a new ask should pass + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: true, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_0 = solana.get_account::(account_0).await; + assert_eq!(mango_account_0.perps[0].asks_base_lots, 2); + + // account_0 - place a new ask should fail if not reduce_only + let res = send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await; + assert!(res.is_err()); + + // account_0 - place a new ask should pass but have no effect + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: true, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_0 = solana.get_account::(account_0).await; + assert_eq!(mango_account_0.perps[0].asks_base_lots, 2); + + // account_1 - place a new ask with reduce only false should fail + let res = send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await; + assert!(res.is_err()); + + // account_1 - place a new ask with reduce only true should pass + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: true, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!(mango_account_1.perps[0].asks_base_lots, 1); + + // account_1 - place a new bid should pass + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Bid, + price_lots: price_lots / 2, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!(mango_account_1.perps[0].bids_base_lots, 1); + + // account_1 - place a new bid should pass + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Bid, + price_lots: price_lots / 2, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!(mango_account_1.perps[0].bids_base_lots, 2); + + // account_1 - place a new bid should fail if reduce only is false + let res = send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Bid, + price_lots: price_lots / 2, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + }, + ) + .await; + assert!(res.is_err()); + + // account_1 - place a new bid should pass but have no effect + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Bid, + price_lots: price_lots / 2, + max_base_lots: 1, + max_quote_lots: i64::MAX, + reduce_only: true, + client_order_id: 0, + }, + ) + .await + .unwrap(); + let mango_account_1 = solana.get_account::(account_1).await; + assert_eq!(mango_account_1.perps[0].bids_base_lots, 2); + + Ok(()) +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 0908fa414..9cd81049d 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -316,6 +316,7 @@ export class MangoClient { depositWeightScaleStartQuote: number | null, resetStablePrice: boolean | null, resetNetBorrowLimit: boolean | null, + reduceOnly: boolean | null, ): Promise { const bank = group.getFirstBankByMint(mintPk); const mintInfo = group.mintInfosMapByTokenIndex.get(bank.tokenIndex)!; @@ -347,6 +348,7 @@ export class MangoClient { depositWeightScaleStartQuote, resetStablePrice ?? false, resetNetBorrowLimit ?? false, + reduceOnly, ) .accounts({ group: group.publicKey, @@ -802,6 +804,7 @@ export class MangoClient { mangoAccount: MangoAccount, mintPk: PublicKey, amount: number, + reduceOnly = false, ): Promise { const decimals = group.getMintDecimals(mintPk); const nativeAmount = toNative(amount, decimals); @@ -810,6 +813,7 @@ export class MangoClient { mangoAccount, mintPk, nativeAmount, + reduceOnly, ); } @@ -818,6 +822,7 @@ export class MangoClient { mangoAccount: MangoAccount, mintPk: PublicKey, nativeAmount: BN, + reduceOnly = false, ): Promise { const bank = group.getFirstBankByMint(mintPk); @@ -868,7 +873,7 @@ export class MangoClient { ); const ix = await this.program.methods - .tokenDeposit(new BN(nativeAmount)) + .tokenDeposit(new BN(nativeAmount), reduceOnly) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, @@ -1571,6 +1576,7 @@ export class MangoClient { stablePriceGrowthLimit: number | null, settlePnlLimitFactor: number | null, settlePnlLimitWindowSize: number | null, + reduceOnly: boolean | null, ): Promise { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); @@ -1602,6 +1608,7 @@ export class MangoClient { settlePnlLimitWindowSize !== null ? new BN(settlePnlLimitWindowSize) : null, + reduceOnly, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index dcb2f1525..edb7403a4 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -644,6 +644,12 @@ export type MangoV4 = { { "name": "resetNetBorrowLimit", "type": "bool" + }, + { + "name": "reduceOnlyOpt", + "type": { + "option": "bool" + } } ] }, @@ -1200,6 +1206,10 @@ export type MangoV4 = { { "name": "amount", "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" } ] }, @@ -1251,6 +1261,10 @@ export type MangoV4 = { { "name": "amount", "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" } ] }, @@ -1511,6 +1525,34 @@ export type MangoV4 = { } ] }, + { + "name": "serum3EditMarket", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "market", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "reduceOnlyOpt", + "type": { + "option": "bool" + } + } + ] + }, { "name": "serum3DeregisterMarket", "accounts": [ @@ -2660,6 +2702,12 @@ export type MangoV4 = { "type": { "option": "u64" } + }, + { + "name": "reduceOnlyOpt", + "type": { + "option": "bool" + } } ] }, @@ -3736,12 +3784,16 @@ export type MangoV4 = { ], "type": "f64" }, + { + "name": "reduceOnly", + "type": "u8" + }, { "name": "reserved", "type": { "array": [ "u8", - 2120 + 2119 ] } } @@ -4430,12 +4482,16 @@ export type MangoV4 = { ], "type": "u64" }, + { + "name": "reduceOnly", + "type": "u8" + }, { "name": "reserved", "type": { "array": [ "u8", - 1944 + 1943 ] } } @@ -4459,12 +4515,16 @@ export type MangoV4 = { "name": "quoteTokenIndex", "type": "u16" }, + { + "name": "reduceOnly", + "type": "u8" + }, { "name": "padding1", "type": { "array": [ "u8", - 4 + 3 ] } }, @@ -7403,6 +7463,21 @@ export type MangoV4 = { "code": 6028, "name": "TokenPositionDoesNotExist", "msg": "token position does not exist" + }, + { + "code": 6029, + "name": "DepositsIntoLiquidatingMustRecover", + "msg": "token deposits into accounts that are being liquidated must bring their health above the init threshold" + }, + { + "code": 6030, + "name": "TokenInReduceOnlyMode", + "msg": "token is in reduce only mode" + }, + { + "code": 6031, + "name": "MarketInReduceOnlyMode", + "msg": "market is in reduce only mode" } ] }; @@ -8053,6 +8128,12 @@ export const IDL: MangoV4 = { { "name": "resetNetBorrowLimit", "type": "bool" + }, + { + "name": "reduceOnlyOpt", + "type": { + "option": "bool" + } } ] }, @@ -8609,6 +8690,10 @@ export const IDL: MangoV4 = { { "name": "amount", "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" } ] }, @@ -8660,6 +8745,10 @@ export const IDL: MangoV4 = { { "name": "amount", "type": "u64" + }, + { + "name": "reduceOnly", + "type": "bool" } ] }, @@ -8920,6 +9009,34 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "serum3EditMarket", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "market", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "reduceOnlyOpt", + "type": { + "option": "bool" + } + } + ] + }, { "name": "serum3DeregisterMarket", "accounts": [ @@ -10069,6 +10186,12 @@ export const IDL: MangoV4 = { "type": { "option": "u64" } + }, + { + "name": "reduceOnlyOpt", + "type": { + "option": "bool" + } } ] }, @@ -11145,12 +11268,16 @@ export const IDL: MangoV4 = { ], "type": "f64" }, + { + "name": "reduceOnly", + "type": "u8" + }, { "name": "reserved", "type": { "array": [ "u8", - 2120 + 2119 ] } } @@ -11839,12 +11966,16 @@ export const IDL: MangoV4 = { ], "type": "u64" }, + { + "name": "reduceOnly", + "type": "u8" + }, { "name": "reserved", "type": { "array": [ "u8", - 1944 + 1943 ] } } @@ -11868,12 +11999,16 @@ export const IDL: MangoV4 = { "name": "quoteTokenIndex", "type": "u16" }, + { + "name": "reduceOnly", + "type": "u8" + }, { "name": "padding1", "type": { "array": [ "u8", - 4 + 3 ] } }, @@ -14812,6 +14947,21 @@ export const IDL: MangoV4 = { "code": 6028, "name": "TokenPositionDoesNotExist", "msg": "token position does not exist" + }, + { + "code": 6029, + "name": "DepositsIntoLiquidatingMustRecover", + "msg": "token deposits into accounts that are being liquidated must bring their health above the init threshold" + }, + { + "code": 6030, + "name": "TokenInReduceOnlyMode", + "msg": "token is in reduce only mode" + }, + { + "code": 6031, + "name": "MarketInReduceOnlyMode", + "msg": "market is in reduce only mode" } ] }; diff --git a/ts/client/src/scripts/mb-admin.ts b/ts/client/src/scripts/mb-admin.ts index 213167cf7..3904fb34a 100644 --- a/ts/client/src/scripts/mb-admin.ts +++ b/ts/client/src/scripts/mb-admin.ts @@ -1,9 +1,13 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { AddressLookupTableProgram, + ComputeBudgetProgram, Connection, Keypair, PublicKey, + SYSVAR_INSTRUCTIONS_PUBKEY, + SYSVAR_RENT_PUBKEY, + SystemProgram, } from '@solana/web3.js'; import fs from 'fs'; import { TokenIndex } from '../accounts/bank'; @@ -14,8 +18,13 @@ import { Serum3Side, } from '../accounts/serum3'; import { MangoClient } from '../client'; -import { MANGO_V4_ID } from '../constants'; +import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID } from '../constants'; import { buildVersionedTx, toNative } from '../utils'; +import { + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + NATIVE_MINT, +} from '@solana/spl-token'; const GROUP_NUM = Number(process.env.GROUP_NUM || 0); @@ -554,11 +563,25 @@ async function createAndPopulateAlt() { } console.log(`ALT: extending using mango v4 relevant public keys`); + await extendTable(bankAddresses); + await extendTable([OPENBOOK_PROGRAM_ID['mainnet-beta']]); await extendTable(serum3MarketAddresses); await extendTable(serum3ExternalMarketAddresses); + // TODO: dont extend for perps atm // await extendTable(perpMarketAddresses); + + // Well known addressess + await extendTable([ + SystemProgram.programId, + SYSVAR_RENT_PUBKEY, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + NATIVE_MINT, + SYSVAR_INSTRUCTIONS_PUBKEY, + ComputeBudgetProgram.programId, + ]); } catch (error) { console.log(error); } @@ -595,7 +618,7 @@ async function main() { } try { - // createAndPopulateAlt(); + createAndPopulateAlt(); } catch (error) {} } diff --git a/ts/client/src/scripts/mb-edit-perp-market.ts b/ts/client/src/scripts/mb-edit-perp-market.ts index d748737a3..c750acfa0 100644 --- a/ts/client/src/scripts/mb-edit-perp-market.ts +++ b/ts/client/src/scripts/mb-edit-perp-market.ts @@ -66,6 +66,7 @@ async function editPerpMarket(perpMarketName: string) { null, null, null, + null, ); console.log('Tx Successful:', signature); diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index 9f066e840..ef8d931e3 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -123,6 +123,7 @@ async function main() { null, true, null, + null, ); } async function setPerpPrice( @@ -158,6 +159,7 @@ async function main() { null, null, null, + null, ); } @@ -262,6 +264,7 @@ async function main() { null, null, null, + null, ); try { // At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have. @@ -304,6 +307,7 @@ async function main() { null, null, null, + null, ); } } 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 886938074..c1ba1d974 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 @@ -10,9 +10,7 @@ import { MANGO_V4_ID } from '../constants'; // const GROUP_NUM = Number(process.env.GROUP_NUM || 200); -const CLUSTER_URL = - process.env.CLUSTER_URL || - 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88'; +const CLUSTER_URL = process.env.CLUSTER_URL; const MANGO_MAINNET_PAYER_KEYPAIR = process.env.MANGO_MAINNET_PAYER_KEYPAIR || '';