diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..b027e1d26 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +mm: node dist/cjs/src/scripts/mm/market-maker.js \ No newline at end of file diff --git a/README.md b/README.md index 80da290c6..606d5302c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ -### Development +## Development -- rust version 1.59.0 (9d1b2106e 2022-02-23) -- solana-cli 1.9.13 -- anchor-cli 0.24.2 +### Dependencies + +- rust version 1.65.0 +- solana-cli 1.14.9 - npm 8.1.2 - node v16.13.1 -Devnet deployment - 4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg +### Submodules + +After cloning this repo you'll need to init and update its git submodules. +Consider setting the git option `submodule.recurse=true`. + +### Deployments + +- devnet: 4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg +- mainnet-beta: 4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg + +### Notes For testing latest program changes while developing, just run below scripts in given order form any branch, 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/liquidator/src/util.rs b/liquidator/src/util.rs index 880562ca2..59b7d3414 100644 --- a/liquidator/src/util.rs +++ b/liquidator/src/util.rs @@ -1,7 +1,7 @@ use anchor_lang::Discriminator; use arrayref::array_ref; -use mango_v4::state::{Bank, MangoAccount, MangoAccountRefWithHeader, MintInfo, PerpMarket}; +use mango_v4::state::{Bank, MangoAccount, MangoAccountLoadedRef, MintInfo, PerpMarket}; use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::pubkey::Pubkey; @@ -10,7 +10,7 @@ pub fn is_mango_account<'a>( account: &'a AccountSharedData, program_id: &Pubkey, group_id: &Pubkey, -) -> Option> { +) -> Option> { let data = account.data(); if account.owner() != program_id || data.len() < 8 { return None; @@ -21,7 +21,7 @@ pub fn is_mango_account<'a>( return None; } - if let Ok(mango_account) = MangoAccountRefWithHeader::from_bytes(&data[8..]) { + if let Ok(mango_account) = MangoAccountLoadedRef::from_bytes(&data[8..]) { if mango_account.fixed.group != *group_id { return None; } diff --git a/package.json b/package.json index 2f5b108f4..35ee08ab1 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,13 @@ "@types/bs58": "^4.0.1", "@types/chai": "^4.3.0", "@types/mocha": "^9.1.0", - "@types/node": "^14.14.37", + "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", - "binance-api-node": "^0.12.0", "chai": "^4.3.4", - "cross-fetch": "^3.1.5", - "dotenv": "^16.0.3", "eslint": "^7.28.0", "eslint-config-prettier": "^7.2.0", - "ftx-api": "^1.1.13", "mocha": "^9.1.3", - "node-kraken-api": "^2.2.2", "prettier": "^2.0.5", "ts-mocha": "^10.0.0", "ts-node": "^9.1.1", @@ -70,7 +65,12 @@ "@solana/web3.js": "^1.63.1", "@switchboard-xyz/sbv2-lite": "^0.1.6", "big.js": "^6.1.1", - "bs58": "^5.0.0" + "bs58": "^5.0.0", + "binance-api-node": "^0.12.0", + "node-kraken-api": "^2.2.2", + "ftx-api": "^1.1.13", + "cross-fetch": "^3.1.5", + "dotenv": "^16.0.3" }, "resolutions": { "@project-serum/anchor/@solana/web3.js": "1.63.1" 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/account_close.rs b/programs/mango-v4/src/instructions/account_close.rs index ee7e34c23..017969a76 100644 --- a/programs/mango-v4/src/instructions/account_close.rs +++ b/programs/mango-v4/src/instructions/account_close.rs @@ -14,7 +14,7 @@ pub struct AccountClose<'info> { has_one = owner, close = sol_destination )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -25,7 +25,7 @@ pub struct AccountClose<'info> { } pub fn account_close(ctx: Context, force_close: bool) -> Result<()> { - let account = ctx.accounts.account.load_mut()?; + let account = ctx.accounts.account.load_full_mut()?; if !ctx.accounts.group.load()?.is_testing() { require!(!force_close, MangoError::SomeError); diff --git a/programs/mango-v4/src/instructions/account_create.rs b/programs/mango-v4/src/instructions/account_create.rs index 3da39f06f..d52e0cf27 100644 --- a/programs/mango-v4/src/instructions/account_create.rs +++ b/programs/mango-v4/src/instructions/account_create.rs @@ -16,7 +16,7 @@ pub struct AccountCreate<'info> { payer = payer, space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count)?, )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -34,7 +34,7 @@ pub fn account_create( perp_oo_count: u8, name: String, ) -> Result<()> { - let mut account = ctx.accounts.account.load_init()?; + let mut account = ctx.accounts.account.load_full_init()?; msg!( "Initialized account with header version {}", diff --git a/programs/mango-v4/src/instructions/account_edit.rs b/programs/mango-v4/src/instructions/account_edit.rs index 52596f7d6..70621c7dc 100644 --- a/programs/mango-v4/src/instructions/account_edit.rs +++ b/programs/mango-v4/src/instructions/account_edit.rs @@ -13,7 +13,7 @@ pub struct AccountEdit<'info> { has_one = group, has_one = owner )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, } @@ -28,7 +28,7 @@ pub fn account_edit( MangoError::SomeError ); - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; // note: unchanged fields are inline, and match exact definition in create_account // please maintain, and don't remove, makes it easy to reason about which support modification by owner diff --git a/programs/mango-v4/src/instructions/account_expand.rs b/programs/mango-v4/src/instructions/account_expand.rs index 8f0397d12..fe655228e 100644 --- a/programs/mango-v4/src/instructions/account_expand.rs +++ b/programs/mango-v4/src/instructions/account_expand.rs @@ -12,7 +12,7 @@ pub struct AccountExpand<'info> { has_one = group, has_one = owner )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -53,7 +53,7 @@ pub fn account_expand( realloc_account.realloc(new_space, false)?; // expand dynamic content, e.g. to grow token positions, we need to slide serum3orders further later, and so on.... - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; account.expand_dynamic_content(token_count, serum3_count, perp_count, perp_oo_count)?; Ok(()) diff --git a/programs/mango-v4/src/instructions/compute_account_data.rs b/programs/mango-v4/src/instructions/compute_account_data.rs index 4acdd586b..6a7732991 100644 --- a/programs/mango-v4/src/instructions/compute_account_data.rs +++ b/programs/mango-v4/src/instructions/compute_account_data.rs @@ -6,13 +6,13 @@ pub struct ComputeAccountData<'info> { pub group: AccountLoader<'info, Group>, #[account(has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, } pub fn compute_account_data(ctx: Context) -> Result<()> { let group_pk = ctx.accounts.group.key(); - let account = ctx.accounts.account.load()?; + let account = ctx.accounts.account.load_full()?; let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?; diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 14355a0dd..9701cf40f 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -3,8 +3,7 @@ use crate::error::*; use crate::group_seeds; use crate::health::{new_fixed_order_account_retriever, new_health_cache, AccountRetriever}; use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog}; -use crate::state::MangoAccount; -use crate::state::{AccountLoaderDynamic, Bank, Group, TokenIndex}; +use crate::state::*; use crate::util::checked_math as cm; use anchor_lang::prelude::*; use anchor_lang::solana_program::sysvar::instructions as tx_instructions; @@ -32,7 +31,7 @@ pub mod jupiter_mainnet_3 { /// 4. the mango group #[derive(Accounts)] pub struct FlashLoanBegin<'info> { - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, // owner is checked at #1 pub owner: Signer<'info>, @@ -55,7 +54,7 @@ pub struct FlashLoanBegin<'info> { #[derive(Accounts)] pub struct FlashLoanEnd<'info> { #[account(mut)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, // owner is checked at #1 pub owner: Signer<'info>, @@ -75,7 +74,7 @@ pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanBegin<'info>>, loan_amounts: Vec, ) -> Result<()> { - let account = ctx.accounts.account.load_mut()?; + let account = ctx.accounts.account.load_full_mut()?; // account constraint #1 require!( @@ -239,7 +238,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanEnd<'info>>, flash_loan_type: FlashLoanType, ) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; // account constraint #1 require!( @@ -377,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() { @@ -387,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) @@ -396,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/health_region.rs b/programs/mango-v4/src/instructions/health_region.rs index 285380c4b..312b75d15 100644 --- a/programs/mango-v4/src/instructions/health_region.rs +++ b/programs/mango-v4/src/instructions/health_region.rs @@ -19,7 +19,7 @@ pub struct HealthRegionBegin<'info> { pub instructions: UncheckedAccount<'info>, #[account(mut)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, } /// Ends a health region. @@ -28,7 +28,7 @@ pub struct HealthRegionBegin<'info> { #[derive(Accounts)] pub struct HealthRegionEnd<'info> { #[account(mut)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, } pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( @@ -69,7 +69,7 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( ); } - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require_msg!( !account.fixed.is_in_health_region(), "account must not already be health wrapped" @@ -91,7 +91,7 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( pub fn health_region_end<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>, ) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require_msg!( account.fixed.is_in_health_region(), "account must be health wrapped" 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_cancel_all_orders.rs b/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs index ae442f57b..3145c79a3 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs @@ -1,14 +1,14 @@ use anchor_lang::prelude::*; use crate::error::MangoError; -use crate::state::{AccountLoaderDynamic, BookSide, Group, MangoAccount, Orderbook, PerpMarket}; +use crate::state::{BookSide, Group, MangoAccountFixed, MangoAccountLoader, Orderbook, PerpMarket}; #[derive(Accounts)] pub struct PerpCancelAllOrders<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -25,7 +25,7 @@ pub struct PerpCancelAllOrders<'info> { } pub fn perp_cancel_all_orders(ctx: Context, limit: u8) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs b/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs index f983d0f6b..75d1befb7 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::error::MangoError; use crate::state::{ - AccountLoaderDynamic, BookSide, Group, MangoAccount, Orderbook, PerpMarket, Side, + BookSide, Group, MangoAccountFixed, MangoAccountLoader, Orderbook, PerpMarket, Side, }; #[derive(Accounts)] @@ -10,7 +10,7 @@ pub struct PerpCancelAllOrdersBySide<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -31,7 +31,7 @@ pub fn perp_cancel_all_orders_by_side( side_option: Option, limit: u8, ) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require_keys_eq!(account.fixed.group, ctx.accounts.group.key()); require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), @@ -40,8 +40,8 @@ pub fn perp_cancel_all_orders_by_side( let mut perp_market = ctx.accounts.perp_market.load_mut()?; let mut book = Orderbook { - bids: ctx.accounts.bids.load_init()?, - asks: ctx.accounts.asks.load_init()?, + bids: ctx.accounts.bids.load_mut()?, + asks: ctx.accounts.asks.load_mut()?, }; book.cancel_all_orders( diff --git a/programs/mango-v4/src/instructions/perp_cancel_order.rs b/programs/mango-v4/src/instructions/perp_cancel_order.rs index c60666eb9..e688c7ec2 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_order.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_order.rs @@ -1,14 +1,14 @@ use anchor_lang::prelude::*; use crate::error::*; -use crate::state::{AccountLoaderDynamic, BookSide, Group, MangoAccount, Orderbook, PerpMarket}; +use crate::state::{BookSide, Group, MangoAccountFixed, MangoAccountLoader, Orderbook, PerpMarket}; #[derive(Accounts)] pub struct PerpCancelOrder<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -25,7 +25,7 @@ pub struct PerpCancelOrder<'info> { } pub fn perp_cancel_order(ctx: Context, order_id: u128) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs b/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs index 0009b607a..f9baaae0e 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs @@ -1,14 +1,14 @@ use anchor_lang::prelude::*; use crate::error::*; -use crate::state::{AccountLoaderDynamic, BookSide, Group, MangoAccount, Orderbook, PerpMarket}; +use crate::state::{BookSide, Group, MangoAccountFixed, MangoAccountLoader, Orderbook, PerpMarket}; #[derive(Accounts)] pub struct PerpCancelOrderByClientOrderId<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -28,7 +28,7 @@ pub fn perp_cancel_order_by_client_order_id( ctx: Context, client_order_id: u64, ) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/perp_consume_events.rs b/programs/mango-v4/src/instructions/perp_consume_events.rs index 68fcbc9d8..411085f78 100644 --- a/programs/mango-v4/src/instructions/perp_consume_events.rs +++ b/programs/mango-v4/src/instructions/perp_consume_events.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use bytemuck::cast_ref; use crate::error::MangoError; -use crate::state::{AccountLoaderDynamic, EventQueue, MangoAccount}; +use crate::state::{EventQueue, MangoAccountFixed, MangoAccountLoader}; use crate::state::{EventType, FillEvent, Group, OutEvent, PerpMarket}; use crate::logs::{emit_perp_balances, FillLog}; @@ -55,9 +55,9 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res continue; } - let mal: AccountLoaderDynamic = - AccountLoaderDynamic::try_from(ai)?; - let mut ma = mal.load_mut()?; + let mal: AccountLoader = + AccountLoader::try_from(ai)?; + let mut ma = mal.load_full_mut()?; ma.execute_perp_maker( perp_market.perp_market_index, &mut perp_market, @@ -90,9 +90,9 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res continue; } - let mal: AccountLoaderDynamic = - AccountLoaderDynamic::try_from(ai)?; - let mut maker = mal.load_mut()?; + let mal: AccountLoader = + AccountLoader::try_from(ai)?; + let mut maker = mal.load_full_mut()?; match mango_account_ais.iter().find(|ai| ai.key == &fill.taker) { None => { @@ -106,9 +106,9 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res continue; } - let mal: AccountLoaderDynamic = - AccountLoaderDynamic::try_from(ai)?; - let mut taker = mal.load_mut()?; + let mal: AccountLoader = + AccountLoader::try_from(ai)?; + let mut taker = mal.load_full_mut()?; maker.execute_perp_maker( perp_market.perp_market_index, @@ -174,9 +174,8 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res continue; } - let mal: AccountLoaderDynamic = - AccountLoaderDynamic::try_from(ai)?; - let mut ma = mal.load_mut()?; + let mal: AccountLoader = AccountLoader::try_from(ai)?; + let mut ma = mal.load_full_mut()?; ma.remove_perp_order(out.owner_slot as usize, out.quantity)?; } 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_deactivate_position.rs b/programs/mango-v4/src/instructions/perp_deactivate_position.rs index fc866bcbf..6c6668c20 100644 --- a/programs/mango-v4/src/instructions/perp_deactivate_position.rs +++ b/programs/mango-v4/src/instructions/perp_deactivate_position.rs @@ -12,7 +12,7 @@ pub struct PerpDeactivatePosition<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(has_one = group)] @@ -20,7 +20,7 @@ pub struct PerpDeactivatePosition<'info> { } pub fn perp_deactivate_position(ctx: Context) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), 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_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs index 33b104ff3..007355976 100644 --- a/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs @@ -28,14 +28,14 @@ pub struct PerpLiqBankruptcy<'info> { has_one = group // liqor_owner is checked at #1 )] - pub liqor: AccountLoaderDynamic<'info, MangoAccount>, + pub liqor: AccountLoader<'info, MangoAccountFixed>, pub liqor_owner: Signer<'info>, #[account( mut, has_one = group )] - pub liqee: AccountLoaderDynamic<'info, MangoAccount>, + pub liqee: AccountLoader<'info, MangoAccountFixed>, #[account( mut, @@ -78,7 +78,7 @@ pub fn perp_liq_bankruptcy(ctx: Context, max_liab_transfer: u let group = ctx.accounts.group.load()?; let group_pk = &ctx.accounts.group.key(); - let mut liqor = ctx.accounts.liqor.load_mut()?; + let mut liqor = ctx.accounts.liqor.load_full_mut()?; // account constraint #1 require!( liqor @@ -88,7 +88,7 @@ pub fn perp_liq_bankruptcy(ctx: Context, max_liab_transfer: u ); require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated); - let mut liqee = ctx.accounts.liqee.load_mut()?; + let mut liqee = ctx.accounts.liqee.load_full_mut()?; let mut liqee_health_cache = { let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?; new_health_cache(&liqee.borrow(), &account_retriever) diff --git a/programs/mango-v4/src/instructions/perp_liq_base_position.rs b/programs/mango-v4/src/instructions/perp_liq_base_position.rs index 311e0a18e..4cfbb4ca4 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_position.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_position.rs @@ -24,11 +24,11 @@ pub struct PerpLiqBasePosition<'info> { has_one = group // liqor_owner is checked at #1 )] - pub liqor: AccountLoaderDynamic<'info, MangoAccount>, + pub liqor: AccountLoader<'info, MangoAccountFixed>, pub liqor_owner: Signer<'info>, #[account(mut, has_one = group)] - pub liqee: AccountLoaderDynamic<'info, MangoAccount>, + pub liqee: AccountLoader<'info, MangoAccountFixed>, } pub fn perp_liq_base_position( @@ -37,7 +37,7 @@ pub fn perp_liq_base_position( ) -> Result<()> { let group_pk = &ctx.accounts.group.key(); - let mut liqor = ctx.accounts.liqor.load_mut()?; + let mut liqor = ctx.accounts.liqor.load_full_mut()?; // account constraint #1 require!( liqor @@ -47,7 +47,7 @@ pub fn perp_liq_base_position( ); require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated); - let mut liqee = ctx.accounts.liqee.load_mut()?; + let mut liqee = ctx.accounts.liqee.load_full_mut()?; // Initial liqee health check let mut liqee_health_cache = { diff --git a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs index 2b6836b40..b33901a1d 100644 --- a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs @@ -10,7 +10,7 @@ pub struct PerpLiqForceCancelOrders<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, #[account( mut, @@ -29,7 +29,7 @@ pub fn perp_liq_force_cancel_orders( ctx: Context, limit: u8, ) -> Result<()> { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; // // Check liqee health if liquidation is allowed diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index ab96e8fa1..cd66742af 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -3,9 +3,10 @@ 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::MangoAccount; +use crate::state::Side; use crate::state::{ - AccountLoaderDynamic, BookSide, EventQueue, Group, Order, Orderbook, PerpMarket, + BookSide, EventQueue, Group, MangoAccountFixed, MangoAccountLoader, Order, Orderbook, + PerpMarket, }; #[derive(Accounts)] @@ -13,7 +14,7 @@ pub struct PerpPlaceOrder<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -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); @@ -64,7 +65,7 @@ pub fn perp_place_order(ctx: Context, order: Order, limit: u8) - perp_market.update_funding_and_stable_price(&book, oracle_price, now_ts)?; } - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError @@ -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/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index f2edeae82..5d4fa5328 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -6,8 +6,7 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::{compute_health, new_fixed_order_account_retriever, HealthType}; use crate::state::Bank; -use crate::state::MangoAccount; -use crate::state::{AccountLoaderDynamic, Group, PerpMarket}; +use crate::state::{Group, MangoAccountFixed, MangoAccountLoader, PerpMarket}; use crate::logs::{emit_perp_balances, PerpSettleFeesLog, TokenBalanceLog}; @@ -20,7 +19,7 @@ pub struct PerpSettleFees<'info> { // This account MUST have a loss #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, /// CHECK: Oracle can have different account types, constrained by address in perp_market pub oracle: UncheckedAccount<'info>, @@ -40,7 +39,7 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> MangoError::MaxSettleAmountMustBeGreaterThanZero ); - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let mut bank = ctx.accounts.settle_bank.load_mut()?; let mut perp_market = ctx.accounts.perp_market.load_mut()?; @@ -105,7 +104,7 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> let token_position = account .token_position_mut(perp_market.settle_token_index)? .0; - bank.withdraw_with_fee( + bank.withdraw_without_fee( token_position, settlement, Clock::get()?.unix_timestamp.try_into().unwrap(), diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index d51fe8b8f..f2d020ea7 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -7,8 +7,7 @@ use crate::error::*; use crate::health::{new_health_cache, HealthType, ScanningAccountRetriever}; use crate::logs::{emit_perp_balances, PerpSettlePnlLog, TokenBalanceLog}; use crate::state::Bank; -use crate::state::MangoAccount; -use crate::state::{AccountLoaderDynamic, Group, PerpMarket}; +use crate::state::{Group, MangoAccountFixed, MangoAccountLoader, PerpMarket}; #[derive(Accounts)] pub struct PerpSettlePnl<'info> { @@ -19,7 +18,7 @@ pub struct PerpSettlePnl<'info> { has_one = group, // settler_owner is checked at #1 )] - pub settler: AccountLoaderDynamic<'info, MangoAccount>, + pub settler: AccountLoader<'info, MangoAccountFixed>, pub settler_owner: Signer<'info>, #[account(has_one = group, has_one = oracle)] @@ -27,10 +26,10 @@ pub struct PerpSettlePnl<'info> { // This account MUST be profitable #[account(mut, has_one = group)] - pub account_a: AccountLoaderDynamic<'info, MangoAccount>, + pub account_a: AccountLoader<'info, MangoAccountFixed>, // This account MUST have a loss #[account(mut, has_one = group)] - pub account_b: AccountLoaderDynamic<'info, MangoAccount>, + pub account_b: AccountLoader<'info, MangoAccountFixed>, /// CHECK: Oracle can have different account types, constrained by address in perp_market pub oracle: UncheckedAccount<'info>, @@ -58,8 +57,8 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { ) }; - let mut account_a = ctx.accounts.account_a.load_mut()?; - let mut account_b = ctx.accounts.account_b.load_mut()?; + let mut account_a = ctx.accounts.account_a.load_full_mut()?; + let mut account_b = ctx.accounts.account_b.load_full_mut()?; // check positions exist, for nicer error messages { @@ -199,7 +198,10 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { let a_token_position = account_a.token_position_mut(settle_token_index)?.0; let b_token_position = account_b.token_position_mut(settle_token_index)?.0; bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?; - bank.withdraw_with_fee(b_token_position, settlement, now_ts, oracle_price)?; + // Don't charge loan origination fees on borrows created via settling: + // Even small loan origination fees could accumulate if a perp position is + // settled back and forth repeatedly. + bank.withdraw_without_fee(b_token_position, settlement, now_ts, oracle_price)?; emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), @@ -223,7 +225,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { drop(account_a); drop(account_b); - let mut settler = ctx.accounts.settler.load_mut()?; + let mut settler = ctx.accounts.settler.load_full_mut()?; // account constraint #1 require!( settler diff --git a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs index 6ac6f95ae..9bb32086d 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs @@ -14,7 +14,7 @@ pub struct Serum3CancelAllOrders<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -51,7 +51,7 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) // Validation // { - let account = ctx.accounts.account.load()?; + let account = ctx.accounts.account.load_full()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index 89645c1f9..428cc7f8e 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -20,7 +20,7 @@ pub struct Serum3CancelOrder<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -63,7 +63,7 @@ pub fn serum3_cancel_order( // Validation // { - let account = ctx.accounts.account.load()?; + let account = ctx.accounts.account.load_full()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), diff --git a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs index 7d0cebe91..d6da507a5 100644 --- a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs @@ -12,7 +12,7 @@ pub struct Serum3CloseOpenOrders<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -39,7 +39,7 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( // // Validation // - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), diff --git a/programs/mango-v4/src/instructions/serum3_create_open_orders.rs b/programs/mango-v4/src/instructions/serum3_create_open_orders.rs index ff6e34e97..fa40cdb14 100644 --- a/programs/mango-v4/src/instructions/serum3_create_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_create_open_orders.rs @@ -12,7 +12,7 @@ pub struct Serum3CreateOpenOrders<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -51,7 +51,7 @@ pub fn serum3_create_open_orders(ctx: Context) -> Result let serum_market = ctx.accounts.serum_market.load()?; - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), 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_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 93f00dd88..bd68ccb48 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -17,7 +17,7 @@ pub struct Serum3LiqForceCancelOrders<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, #[account(mut)] /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 @@ -77,7 +77,7 @@ pub fn serum3_liq_force_cancel_orders( // let serum_market = ctx.accounts.serum_market.load()?; { - let account = ctx.accounts.account.load()?; + let account = ctx.accounts.account.load_full()?; // Validate open_orders #2 require!( @@ -113,7 +113,7 @@ pub fn serum3_liq_force_cancel_orders( // Check liqee health if liquidation is allowed // let mut health_cache = { - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; let health_cache = @@ -146,7 +146,7 @@ pub fn serum3_liq_force_cancel_orders( let before_oo = { let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; let before_oo = OpenOrdersSlim::from_oo(&open_orders); - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; charge_loan_origination_fees( @@ -208,7 +208,7 @@ pub fn serum3_liq_force_cancel_orders( require_gte!(after_quote_vault, before_quote_vault); // Credit the difference in vault balances to the user's account - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 5c89ba9ed..503508886 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -142,7 +142,7 @@ pub struct Serum3PlaceOrder<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -212,12 +212,16 @@ 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 // { - let account = ctx.accounts.account.load()?; + let account = ctx.accounts.account.load_full()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), @@ -246,7 +250,7 @@ pub fn serum3_place_order( // // Pre-health computation // - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let pre_health_opt = if !account.fixed.is_in_health_region() { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; 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/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index d4f8fadc1..d4deab75d 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -20,7 +20,7 @@ pub struct Serum3SettleFunds<'info> { has_one = group // owner is checked at #1 )] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account(mut)] @@ -76,7 +76,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { // Validation // { - let account = ctx.accounts.account.load()?; + let account = ctx.accounts.account.load_full()?; // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), @@ -119,7 +119,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { { let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; let before_oo = OpenOrdersSlim::from_oo(&open_orders); - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; charge_loan_origination_fees( @@ -155,7 +155,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { require_gte!(after_quote_vault, before_quote_vault); // Credit the difference in vault balances to the user's account - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index f31d028e8..ce035600c 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -18,7 +18,7 @@ pub struct TokenDepositIntoExisting<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, #[account( mut, @@ -48,7 +48,7 @@ pub struct TokenDeposit<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group, has_one = owner)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -76,7 +76,7 @@ pub struct TokenDeposit<'info> { struct DepositCommon<'a, 'info> { pub group: &'a AccountLoader<'info, Group>, - pub account: &'a AccountLoaderDynamic<'info, MangoAccount>, + pub account: &'a AccountLoader<'info, MangoAccountFixed>, pub bank: &'a AccountLoader<'info, Bank>, pub vault: &'a Account<'info, TokenAccount>, pub oracle: &'a UncheckedAccount<'info>, @@ -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_mut()?; + 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,10 +211,10 @@ 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_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; account.ensure_token_position(token_index)?; } @@ -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_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index df07a4179..eef154b9c 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -30,14 +30,14 @@ pub struct TokenLiqBankruptcy<'info> { has_one = group // liqor_owner is checked at #1 )] - pub liqor: AccountLoaderDynamic<'info, MangoAccount>, + pub liqor: AccountLoader<'info, MangoAccountFixed>, pub liqor_owner: Signer<'info>, #[account( mut, has_one = group )] - pub liqee: AccountLoaderDynamic<'info, MangoAccount>, + pub liqee: AccountLoader<'info, MangoAccountFixed>, #[account( has_one = group, @@ -81,7 +81,7 @@ pub fn token_liq_bankruptcy( let (bank_ais, health_ais) = &ctx.remaining_accounts.split_at(liab_mint_info.num_banks()); liab_mint_info.verify_banks_ais(bank_ais)?; - let mut liqor = ctx.accounts.liqor.load_mut()?; + let mut liqor = ctx.accounts.liqor.load_full_mut()?; // account constraint #1 require!( liqor @@ -93,7 +93,7 @@ pub fn token_liq_bankruptcy( let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?; - let mut liqee = ctx.accounts.liqee.load_mut()?; + let mut liqee = ctx.accounts.liqee.load_full_mut()?; let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever) .context("create liqee health cache")?; require!( diff --git a/programs/mango-v4/src/instructions/token_liq_with_token.rs b/programs/mango-v4/src/instructions/token_liq_with_token.rs index a5c4760bc..f1c62d651 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -20,14 +20,14 @@ pub struct TokenLiqWithToken<'info> { has_one = group // liqor_owner is checked at #1 )] - pub liqor: AccountLoaderDynamic<'info, MangoAccount>, + pub liqor: AccountLoader<'info, MangoAccountFixed>, pub liqor_owner: Signer<'info>, #[account( mut, has_one = group )] - pub liqee: AccountLoaderDynamic<'info, MangoAccount>, + pub liqee: AccountLoader<'info, MangoAccountFixed>, } pub fn token_liq_with_token( @@ -42,7 +42,7 @@ pub fn token_liq_with_token( let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk) .context("create account retriever")?; - let mut liqor = ctx.accounts.liqor.load_mut()?; + let mut liqor = ctx.accounts.liqor.load_full_mut()?; // account constraint #1 require!( liqor @@ -52,7 +52,7 @@ pub fn token_liq_with_token( ); require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated); - let mut liqee = ctx.accounts.liqee.load_mut()?; + let mut liqee = ctx.accounts.liqee.load_full_mut()?; // Initial liqee health check let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever) 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 657227763..9dfbc6319 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -18,7 +18,7 @@ pub struct TokenWithdraw<'info> { pub group: AccountLoader<'info, Group>, #[account(mut, has_one = group, has_one = owner)] - pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub account: AccountLoader<'info, MangoAccountFixed>, pub owner: Signer<'info>, #[account( @@ -62,7 +62,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let token_index = ctx.accounts.bank.load()?.token_index; // Create the account's position for that token index - let mut account = ctx.accounts.account.load_mut()?; + let mut account = ctx.accounts.account.load_full_mut()?; let (_, raw_token_index, _) = account.ensure_token_position(token_index)?; // Health check _after_ the token position is guaranteed to exist @@ -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/dynamic_account.rs b/programs/mango-v4/src/state/dynamic_account.rs index 0a5047b42..e4bc2e6a5 100644 --- a/programs/mango-v4/src/state/dynamic_account.rs +++ b/programs/mango-v4/src/state/dynamic_account.rs @@ -1,25 +1,15 @@ use std::cell::{Ref, RefMut}; -use std::marker::PhantomData; -use std::mem::size_of; - use anchor_lang::prelude::*; -use anchor_lang::Discriminator; -use arrayref::array_ref; -// Header is created by scanning and parsing dynamic portion of the account -// Header stores useful information e.g. offsets to easily seek into dynamic content +/// Header is created by scanning and parsing the dynamic portion of the account. +/// This stores useful information e.g. offsets to easily seek into dynamic content. pub trait DynamicHeader: Sized { - // build header by scanning and parsing dynamic portion of the account - fn from_bytes(data: &[u8]) -> Result; + /// Builds header by scanning and parsing the dynamic portion of the account. + fn from_bytes(dynamic_data: &[u8]) -> Result; - // initialize a header on a new account, if necessary - fn initialize(data: &mut [u8]) -> Result<()>; -} - -pub trait DynamicAccountType: Owner + Discriminator { - type Header: DynamicHeader; - type Fixed: bytemuck::Pod; + /// initializes a header on the dynamic portion of a new account + fn initialize(dynamic_data: &mut [u8]) -> Result<()>; } #[derive(Clone)] @@ -29,19 +19,6 @@ pub struct DynamicAccount { pub dynamic: Dynamic, } -pub type DynamicAccountValue = - DynamicAccount<::Header, ::Fixed, Vec>; -pub type DynamicAccountRef<'a, D> = DynamicAccount< - &'a ::Header, - &'a ::Fixed, - &'a [u8], ->; -pub type DynamicAccountRefMut<'a, D> = DynamicAccount< - &'a mut ::Header, - &'a mut ::Fixed, - &'a mut [u8], ->; - // Want to generalize over: // - T (which is Borrow) // - &T (which is Borrow and Deref) @@ -113,199 +90,3 @@ impl DerefOrBorrowMut<[T]> for Vec { self } } - -pub struct AccountLoaderDynamic<'info, D: DynamicAccountType> { - /// CHECK: is checked below - acc_info: AccountInfo<'info>, - phantom1: PhantomData<&'info D>, -} - -impl<'info, D: DynamicAccountType> AccountLoaderDynamic<'info, D> { - pub fn try_from(acc_info: &AccountInfo<'info>) -> Result { - if acc_info.owner != &D::owner() { - return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) - .with_pubkeys((*acc_info.owner, D::owner()))); - } - - let data = acc_info.try_borrow_data()?; - if data.len() < D::discriminator().len() { - return Err(ErrorCode::AccountDiscriminatorNotFound.into()); - } - let disc_bytes = array_ref![data, 0, 8]; - if disc_bytes != &D::discriminator() { - return Err(ErrorCode::AccountDiscriminatorMismatch.into()); - } - - Ok(Self { - acc_info: acc_info.clone(), - phantom1: PhantomData, - }) - } - - pub fn try_from_unchecked(acc_info: &AccountInfo<'info>) -> Result { - if acc_info.owner != &D::owner() { - return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) - .with_pubkeys((*acc_info.owner, D::owner()))); - } - Ok(Self { - acc_info: acc_info.clone(), - phantom1: PhantomData, - }) - } - - /// Returns a Ref to the account data structure for reading. - pub fn load_fixed(&self) -> Result> { - let data = self.acc_info.try_borrow_data()?; - let fixed = Ref::map(data, |d| { - bytemuck::from_bytes(&d[8..8 + size_of::()]) - }); - Ok(fixed) - } - - #[allow(clippy::type_complexity)] - /// Returns a Ref to the account data structure for reading. - pub fn load(&self) -> Result, Ref<[u8]>>> { - let data = self.acc_info.try_borrow_data()?; - let header = D::Header::from_bytes(&data[8 + size_of::()..])?; - let (_, data) = Ref::map_split(data, |d| d.split_at(8)); - let (fixed_bytes, dynamic) = Ref::map_split(data, |d| d.split_at(size_of::())); - Ok(DynamicAccount { - header, - fixed: Ref::map(fixed_bytes, |b| bytemuck::from_bytes(b)), - dynamic, - }) - } - - #[allow(clippy::type_complexity)] - pub fn load_init(&self) -> Result, RefMut<[u8]>>> { - if !self.acc_info.is_writable { - return Err(ErrorCode::AccountNotMutable.into()); - } - - let mut data = self.acc_info.try_borrow_mut_data()?; - let mut disc_bytes = [0u8; 8]; - disc_bytes.copy_from_slice(&data[..8]); - let discriminator = u64::from_le_bytes(disc_bytes); - if discriminator != 0 { - return Err(ErrorCode::AccountDiscriminatorAlreadySet.into()); - } - - let disc_bytes: &mut [u8] = &mut data[0..8]; - disc_bytes.copy_from_slice(bytemuck::bytes_of(&(D::discriminator()))); - - D::Header::initialize(&mut data[8 + size_of::()..])?; - - drop(data); - - self.load_mut() - } - - /// Returns a Ref to the account data structure for reading. - #[allow(clippy::type_complexity)] - pub fn load_mut(&self) -> Result, RefMut<[u8]>>> { - if !self.acc_info.is_writable { - return Err(ErrorCode::AccountNotMutable.into()); - } - - let data = self.acc_info.try_borrow_mut_data()?; - let header = D::Header::from_bytes(&data[8 + size_of::()..])?; - let (_, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); - let (fixed_bytes, dynamic) = - RefMut::map_split(data, |d| d.split_at_mut(size_of::())); - Ok(DynamicAccount { - header, - fixed: RefMut::map(fixed_bytes, |b| bytemuck::from_bytes_mut(b)), - dynamic, - }) - } -} - -impl<'info, D: DynamicAccountType> anchor_lang::Accounts<'info> for AccountLoaderDynamic<'info, D> { - #[inline(never)] - fn try_accounts( - _program_id: &Pubkey, - accounts: &mut &[AccountInfo<'info>], - _ix_data: &[u8], - _bumps: &mut std::collections::BTreeMap, - _reallocs: &mut std::collections::BTreeSet, - ) -> Result { - if accounts.is_empty() { - return Err(ErrorCode::AccountNotEnoughKeys.into()); - } - let account = &accounts[0]; - *accounts = &accounts[1..]; - let l = AccountLoaderDynamic::try_from(account)?; - Ok(l) - } -} - -impl<'info, D: DynamicAccountType> anchor_lang::AccountsExit<'info> - for AccountLoaderDynamic<'info, D> -{ - fn exit(&self, _program_id: &Pubkey) -> Result<()> { - // Normally anchor writes the discriminator again here, but I don't see why - let data = self.acc_info.try_borrow_data()?; - if data.len() < D::discriminator().len() { - return Err(ErrorCode::AccountDiscriminatorNotFound.into()); - } - let disc_bytes = array_ref![data, 0, 8]; - if disc_bytes != &D::discriminator() { - return Err(ErrorCode::AccountDiscriminatorMismatch.into()); - } - Ok(()) - } -} - -impl<'info, D: DynamicAccountType> anchor_lang::AccountsClose<'info> - for AccountLoaderDynamic<'info, D> -{ - fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> { - close(self.to_account_info(), sol_destination) - } -} - -impl<'info, D: DynamicAccountType> anchor_lang::ToAccountMetas for AccountLoaderDynamic<'info, D> { - fn to_account_metas(&self, is_signer: Option) -> Vec { - let is_signer = is_signer.unwrap_or(self.acc_info.is_signer); - let meta = match self.acc_info.is_writable { - false => AccountMeta::new_readonly(*self.acc_info.key, is_signer), - true => AccountMeta::new(*self.acc_info.key, is_signer), - }; - vec![meta] - } -} - -impl<'info, D: DynamicAccountType> AsRef> for AccountLoaderDynamic<'info, D> { - fn as_ref(&self) -> &AccountInfo<'info> { - &self.acc_info - } -} - -impl<'info, D: DynamicAccountType> anchor_lang::ToAccountInfos<'info> - for AccountLoaderDynamic<'info, D> -{ - fn to_account_infos(&self) -> Vec> { - vec![self.acc_info.clone()] - } -} - -impl<'info, D: DynamicAccountType> anchor_lang::Key for AccountLoaderDynamic<'info, D> { - fn key(&self) -> Pubkey { - *self.acc_info.key - } -} - -// https://github.com/coral-xyz/anchor/blob/master/lang/src/common.rs#L8 -fn close<'info>(info: AccountInfo<'info>, sol_destination: AccountInfo<'info>) -> Result<()> { - // Transfer tokens from the account to the sol_destination. - let dest_starting_lamports = sol_destination.lamports(); - **sol_destination.lamports.borrow_mut() = - dest_starting_lamports.checked_add(info.lamports()).unwrap(); - **info.lamports.borrow_mut() = 0; - // Mark the account discriminator as closed. - let mut data = info.try_borrow_mut_data()?; - let dst: &mut [u8] = &mut data; - dst[0..8].copy_from_slice(&[255, 255, 255, 255, 255, 255, 255, 255]); - - Ok(()) -} diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 71c236c34..9b82e4a53 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1,6 +1,8 @@ +use std::cell::{Ref, RefMut}; use std::mem::size_of; use anchor_lang::prelude::*; +use anchor_lang::Discriminator; use arrayref::array_ref; use fixed::types::I80F48; @@ -248,11 +250,20 @@ impl MangoAccountFixed { } } -impl DynamicAccountType for MangoAccount { - type Header = MangoAccountDynamicHeader; - type Fixed = MangoAccountFixed; +impl Owner for MangoAccountFixed { + fn owner() -> Pubkey { + MangoAccount::owner() + } } +impl Discriminator for MangoAccountFixed { + fn discriminator() -> [u8; 8] { + MangoAccount::discriminator() + } +} + +impl anchor_lang::ZeroCopy for MangoAccountFixed {} + #[derive(Clone)] pub struct MangoAccountDynamicHeader { pub token_count: u8, @@ -262,34 +273,34 @@ pub struct MangoAccountDynamicHeader { } impl DynamicHeader for MangoAccountDynamicHeader { - fn from_bytes(data: &[u8]) -> Result { - let header_version = u8::from_le_bytes(*array_ref![data, 0, size_of::()]); + fn from_bytes(dynamic_data: &[u8]) -> Result { + let header_version = u8::from_le_bytes(*array_ref![dynamic_data, 0, size_of::()]); match header_version { 1 => { let token_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ - data, + dynamic_data, MangoAccount::dynamic_token_vec_offset(), BORSH_VEC_SIZE_BYTES ])) .unwrap(); let serum3_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ - data, + dynamic_data, MangoAccount::dynamic_serum3_vec_offset(token_count), BORSH_VEC_SIZE_BYTES ])) .unwrap(); let perp_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ - data, + dynamic_data, MangoAccount::dynamic_perp_vec_offset(token_count, serum3_count), BORSH_VEC_SIZE_BYTES ])) .unwrap(); let perp_oo_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ - data, + dynamic_data, MangoAccount::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count), BORSH_VEC_SIZE_BYTES ])) @@ -306,8 +317,8 @@ impl DynamicHeader for MangoAccountDynamicHeader { } } - fn initialize(data: &mut [u8]) -> Result<()> { - let dst: &mut [u8] = &mut data[0..1]; + fn initialize(dynamic_data: &mut [u8]) -> Result<()> { + let dst: &mut [u8] = &mut dynamic_data[0..1]; dst.copy_from_slice(&DEFAULT_MANGO_ACCOUNT_VERSION.to_le_bytes()); Ok(()) } @@ -368,11 +379,25 @@ impl MangoAccountDynamicHeader { } } -pub type MangoAccountValue = DynamicAccountValue; -pub type MangoAccountRef<'a> = DynamicAccountRef<'a, MangoAccount>; -pub type MangoAccountRefMut<'a> = DynamicAccountRefMut<'a, MangoAccount>; -pub type MangoAccountRefWithHeader<'a> = +/// Fully owned MangoAccount, useful for tests +pub type MangoAccountValue = DynamicAccount>; + +/// Full reference type, useful for borrows +pub type MangoAccountRef<'a> = + DynamicAccount<&'a MangoAccountDynamicHeader, &'a MangoAccountFixed, &'a [u8]>; +/// Full reference type, useful for borrows +pub type MangoAccountRefMut<'a> = + DynamicAccount<&'a mut MangoAccountDynamicHeader, &'a mut MangoAccountFixed, &'a mut [u8]>; + +/// Useful when loading from bytes +pub type MangoAccountLoadedRef<'a> = DynamicAccount; +/// Useful when loading from RefCell, like from AccountInfo +pub type MangoAccountLoadedRefCell<'a> = + DynamicAccount, Ref<'a, [u8]>>; +/// Useful when loading from RefCell, like from AccountInfo +pub type MangoAccountLoadedRefCellMut<'a> = + DynamicAccount, RefMut<'a, [u8]>>; impl MangoAccountValue { // bytes without discriminator @@ -386,7 +411,7 @@ impl MangoAccountValue { } } -impl<'a> MangoAccountRefWithHeader<'a> { +impl<'a> MangoAccountLoadedRef<'a> { // bytes without discriminator pub fn from_bytes(bytes: &'a [u8]) -> Result { let (fixed, dynamic) = bytes.split_at(size_of::()); @@ -399,7 +424,7 @@ impl<'a> MangoAccountRefWithHeader<'a> { } // This generic impl covers MangoAccountRef, MangoAccountRefMut and other -// DynamicAccountValue variants that allow read access. +// DynamicAccount variants that allow read access. impl< Header: DerefOrBorrow, Fixed: DerefOrBorrow, @@ -541,8 +566,8 @@ impl< self.fixed().being_liquidated() } - pub fn borrow(&self) -> DynamicAccountRef { - DynamicAccount { + pub fn borrow(&self) -> MangoAccountRef { + MangoAccountRef { header: self.header(), fixed: self.fixed(), dynamic: self.dynamic(), @@ -566,8 +591,8 @@ impl< self.dynamic.deref_or_borrow_mut() } - pub fn borrow_mut(&mut self) -> DynamicAccountRefMut { - DynamicAccount { + pub fn borrow_mut(&mut self) -> MangoAccountRefMut { + MangoAccountRefMut { header: self.header.deref_or_borrow_mut(), fixed: self.fixed.deref_or_borrow_mut(), dynamic: self.dynamic.deref_or_borrow_mut(), @@ -1086,6 +1111,65 @@ impl< } } +/// Trait to allow a AccountLoader to create an accessor for the full account. +pub trait MangoAccountLoader<'a> { + fn load_full(self) -> Result>; + fn load_full_mut(self) -> Result>; + fn load_full_init(self) -> Result>; +} + +impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAccountFixed> { + fn load_full(self) -> Result> { + // Error checking + self.load()?; + + let data = self.as_ref().try_borrow_data()?; + let header = + MangoAccountDynamicHeader::from_bytes(&data[8 + size_of::()..])?; + let (_, data) = Ref::map_split(data, |d| d.split_at(8)); + let (fixed_bytes, dynamic) = + Ref::map_split(data, |d| d.split_at(size_of::())); + Ok(MangoAccountLoadedRefCell { + header, + fixed: Ref::map(fixed_bytes, |b| bytemuck::from_bytes(b)), + dynamic, + }) + } + + fn load_full_mut(self) -> Result> { + // Error checking + self.load_mut()?; + + let data = self.as_ref().try_borrow_mut_data()?; + let header = + MangoAccountDynamicHeader::from_bytes(&data[8 + size_of::()..])?; + let (_, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); + let (fixed_bytes, dynamic) = + RefMut::map_split(data, |d| d.split_at_mut(size_of::())); + Ok(MangoAccountLoadedRefCellMut { + header, + fixed: RefMut::map(fixed_bytes, |b| bytemuck::from_bytes_mut(b)), + dynamic, + }) + } + + fn load_full_init(self) -> Result> { + // Error checking + self.load_init()?; + + { + let mut data = self.as_ref().try_borrow_mut_data()?; + + let disc_bytes: &mut [u8] = &mut data[0..8]; + disc_bytes.copy_from_slice(bytemuck::bytes_of(&(MangoAccount::discriminator()))); + + MangoAccountDynamicHeader::initialize(&mut data[8 + size_of::()..])?; + } + + self.load_full_mut() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 5ee36b568..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 } @@ -439,7 +444,10 @@ impl PerpPosition { pub fn update_settle_limit(&mut self, market: &PerpMarket, now_ts: u64) { assert_eq!(self.market_index, market.perp_market_index); let window_size = market.settle_pnl_limit_window_size_ts; - let new_window = now_ts >= cm!((self.settle_pnl_limit_window + 1) as u64 * window_size); + let window_start = cm!(self.settle_pnl_limit_window as u64 * window_size); + let window_end = cm!(window_start + window_size); + // now_ts < window_start can happen when window size is changed on the market + let new_window = now_ts >= window_end || now_ts < window_start; if new_window { self.settle_pnl_limit_window = cm!(now_ts / window_size).try_into().unwrap(); self.settle_pnl_limit_settled_in_current_window_native = 0; @@ -855,6 +863,34 @@ mod tests { assert_eq!(pos.realized_pnl_native, I80F48::from(0)); } + #[test] + fn test_perp_settle_limit_window() { + let mut market = PerpMarket::default_for_tests(); + let mut pos = create_perp_position(&market, 100, -50); + + market.settle_pnl_limit_window_size_ts = 100; + pos.settle_pnl_limit_settled_in_current_window_native = 10; + + pos.update_settle_limit(&market, 505); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 0); + assert_eq!(pos.settle_pnl_limit_window, 5); + + pos.settle_pnl_limit_settled_in_current_window_native = 10; + pos.update_settle_limit(&market, 550); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 10); + assert_eq!(pos.settle_pnl_limit_window, 5); + + pos.settle_pnl_limit_settled_in_current_window_native = 10; + pos.update_settle_limit(&market, 600); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 0); + assert_eq!(pos.settle_pnl_limit_window, 6); + + market.settle_pnl_limit_window_size_ts = 400; + pos.update_settle_limit(&market, 605); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 0); + assert_eq!(pos.settle_pnl_limit_window, 1); + } + #[test] fn test_perp_settle_limit() { let market = PerpMarket::default_for_tests(); 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/accounts/bank.ts b/ts/client/src/accounts/bank.ts index acccb5a75..aee5892b0 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -113,6 +113,7 @@ export class Bank implements BankForHealth { netBorrowsInWindow: BN; borrowWeightScaleStartQuote: number; depositWeightScaleStartQuote: number; + reduceOnly: number; }, ): Bank { return new Bank( @@ -158,6 +159,7 @@ export class Bank implements BankForHealth { obj.netBorrowsInWindow, obj.borrowWeightScaleStartQuote, obj.depositWeightScaleStartQuote, + obj.reduceOnly == 1, ); } @@ -204,6 +206,7 @@ export class Bank implements BankForHealth { public netBorrowsInWindow: BN, public borrowWeightScaleStartQuote: number, public depositWeightScaleStartQuote: number, + public reduceOnly: boolean, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 6636e8707..ebaf817dd 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -94,6 +94,7 @@ export class PerpMarket { settleFeeFractionLowHealth: number; settlePnlLimitFactor: number; settlePnlLimitWindowSizeTs: BN; + reduceOnly: number; }, ): PerpMarket { return new PerpMarket( @@ -137,6 +138,7 @@ export class PerpMarket { obj.settleFeeFractionLowHealth, obj.settlePnlLimitFactor, obj.settlePnlLimitWindowSizeTs, + obj.reduceOnly == 1, ); } @@ -181,6 +183,7 @@ export class PerpMarket { public settleFeeFractionLowHealth: number, settlePnlLimitFactor: number, settlePnlLimitWindowSizeTs: BN, + public reduceOnly: boolean, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index a72c6dae3..554a79c83 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -24,6 +24,7 @@ export class Serum3Market { serumMarketExternal: PublicKey; marketIndex: number; registrationTime: BN; + reduceOnly: number; }, ): Serum3Market { return new Serum3Market( @@ -36,6 +37,7 @@ export class Serum3Market { obj.serumMarketExternal, obj.marketIndex as MarketIndex, obj.registrationTime, + obj.reduceOnly == 1, ); } @@ -49,6 +51,7 @@ export class Serum3Market { public serumMarketExternal: PublicKey, public marketIndex: MarketIndex, public registrationTime: BN, + public reduceOnly: boolean, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; } 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 b24d831dc..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); @@ -44,8 +53,11 @@ const MAINNET_SERUM3_MARKETS = new Map([ ['SOL/USDC', '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'], ]); -const { MB_CLUSTER_URL, MB_PAYER_KEYPAIR, MB_USER_KEYPAIR, MB_USER4_KEYPAIR } = - process.env; +const { + MB_CLUSTER_URL, + MB_PAYER_KEYPAIR, + MB_PAYER3_KEYPAIR: MB_PAYER2_KEYPAIR, +} = process.env; const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60; @@ -67,7 +79,7 @@ const defaultInterestRate = { async function buildAdminClient(): Promise<[MangoClient, Keypair]> { const admin = Keypair.fromSecretKey( - Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))), + Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER2_KEYPAIR!, 'utf-8'))), ); const options = AnchorProvider.defaultOptions(); @@ -76,17 +88,17 @@ async function buildAdminClient(): Promise<[MangoClient, Keypair]> { const adminWallet = new Wallet(admin); console.log(`Admin ${adminWallet.publicKey.toBase58()}`); const adminProvider = new AnchorProvider(connection, adminWallet, options); - return [ - await MangoClient.connect( - adminProvider, - 'mainnet-beta', - MANGO_V4_ID['mainnet-beta'], - { - idsSource: 'get-program-accounts', - }, - ), - admin, - ]; + + const client = await MangoClient.connect( + adminProvider, + 'mainnet-beta', + MANGO_V4_ID['mainnet-beta'], + { + idsSource: 'get-program-accounts', + }, + ); + + return [client, admin]; } async function buildUserClient( @@ -107,11 +119,11 @@ async function buildUserClient( MANGO_V4_ID['mainnet-beta'], ); - const admin = Keypair.fromSecretKey( + const creator = Keypair.fromSecretKey( Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))), ); - console.log(`Admin ${admin.publicKey.toBase58()}`); - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + console.log(`Creator ${creator.publicKey.toBase58()}`); + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); return [client, group, user]; } @@ -436,56 +448,16 @@ async function registerPerpMarkets() { ); } -async function main() { - try { - // await createGroup(); - // await changeAdmin(); - } catch (error) { - console.log(error); - } - try { - // await registerTokens(); - } catch (error) { - console.log(error); - } - try { - // await registerSerum3Markets(); - } catch (error) { - console.log(error); - } - - try { - // await registerPerpMarkets(); - } catch (error) { - console.log(error); - } - try { - // await createUser(MB_USER_KEYPAIR!); - // depositForUser(MB_USER_KEYPAIR!); - } catch (error) { - console.log(error); - } - - try { - } catch (error) {} -} - -try { - main(); -} catch (error) { - console.log(error); -} - -//////////////////////////////////////////////////////////// -/// UNUSED ///////////////////////////////////////////////// -//////////////////////////////////////////////////////////// - async function createAndPopulateAlt() { const result = await buildAdminClient(); const client = result[0]; const admin = result[1]; - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + const creator = Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))), + ); + console.log(`Creator ${creator.publicKey.toBase58()}`); + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); const connection = client.program.provider.connection; @@ -591,15 +563,75 @@ 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); - await extendTable(perpMarketAddresses); + + // 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); } } +async function main() { + try { + // await createGroup(); + // await changeAdmin(); + } catch (error) { + console.log(error); + } + try { + // await registerTokens(); + } catch (error) { + console.log(error); + } + try { + // await registerSerum3Markets(); + } catch (error) { + console.log(error); + } + + try { + // await registerPerpMarkets(); + } catch (error) { + console.log(error); + } + try { + // await createUser(MB_USER_KEYPAIR!); + // depositForUser(MB_USER_KEYPAIR!); + } catch (error) { + console.log(error); + } + + try { + createAndPopulateAlt(); + } catch (error) {} +} + +try { + main(); +} catch (error) { + console.log(error); +} + +//////////////////////////////////////////////////////////// +/// UNUSED ///////////////////////////////////////////////// +//////////////////////////////////////////////////////////// + async function expandMangoAccount(userKeypair: string) { const result = await buildUserClient(userKeypair); const client = result[0]; 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 || ''; diff --git a/ts/client/src/scripts/mm/market-maker.ts b/ts/client/src/scripts/mm/market-maker.ts index f63e9091c..cd39d5e67 100644 --- a/ts/client/src/scripts/mm/market-maker.ts +++ b/ts/client/src/scripts/mm/market-maker.ts @@ -28,6 +28,7 @@ import { makeInitSequenceEnforcerAccountIx, seqEnforcerProgramIds, } from './sequence-enforcer-util'; +import * as defaultParams from './params/default.json'; // Future // * use async nodejs logging @@ -229,7 +230,11 @@ async function fullMarketMaker() { const options = AnchorProvider.defaultOptions(); const connection = new Connection(CLUSTER_URL!, options); const user = Keypair.fromSecretKey( - Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))), + Buffer.from( + JSON.parse( + process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'), + ), + ), ); const userWallet = new Wallet(user); const userProvider = new AnchorProvider(connection, userWallet, options); diff --git a/ts/client/src/scripts/mm/params/default.json b/ts/client/src/scripts/mm/params/default.json index e84ea7e2c..a681de704 100644 --- a/ts/client/src/scripts/mm/params/default.json +++ b/ts/client/src/scripts/mm/params/default.json @@ -5,7 +5,7 @@ "assets": { "BTC": { "perp": { - "sizePerc": 0.05, + "sizePerc": 0.75, "leanCoeff": 0.00025, "bias": 0.0, "requoteThresh": 0.0002, @@ -13,17 +13,6 @@ "spammerCharge": 2, "krakenCode": "XXBTZUSD" } - }, - "MNGO": { - "perp": { - "sizePerc": 0.05, - "leanCoeff": 0.00025, - "bias": 0.0, - "requoteThresh": 0.0002, - "takeSpammers": true, - "spammerCharge": 2, - "krakenCode": "MNGOUSD" - } } } } diff --git a/tsconfig.json b/tsconfig.json index 6e3534ec1..e5d8f227a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "esModuleInterop": true, "moduleResolution": "node", "lib": [ - "es2019" + "es2019", + "dom" ], "outDir": "./dist", "resolveJsonModule": true, diff --git a/yarn.lock b/yarn.lock index 8b92cac0b..3d3520907 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,10 +675,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== -"@types/node@^14.14.37": - version "14.18.12" - resolved "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz" - integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A== +"@types/node@^18.11.18": + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== "@types/promise-retry@^1.1.3": version "1.1.3"