use std::cell::{Ref, RefMut}; use std::mem::size_of; use anchor_lang::prelude::*; use anchor_lang::Discriminator; use arrayref::array_ref; use derivative::Derivative; use fixed::types::I80F48; use solana_program::program_memory::sol_memmove; use static_assertions::const_assert_eq; use crate::error::*; use crate::health::{HealthCache, HealthType}; use crate::logs::{emit_stack, DeactivatePerpPositionLog, DeactivateTokenPositionLog}; use crate::util; use super::BookSideOrderTree; use super::FillEvent; use super::LeafNode; use super::PerpMarket; use super::PerpMarketIndex; use super::PerpOpenOrder; use super::Serum3MarketIndex; use super::TokenConditionalSwap; use super::TokenIndex; use super::FREE_ORDER_SLOT; use super::{dynamic_account::*, Group}; use super::{PerpPosition, Serum3Orders, TokenPosition}; use super::{Side, SideAndOrderTree}; type BorshVecLength = u32; const BORSH_VEC_PADDING_BYTES: usize = 4; const BORSH_VEC_SIZE_BYTES: usize = 4; const DEFAULT_MANGO_ACCOUNT_VERSION: u8 = 1; const DYNAMIC_RESERVED_BYTES: usize = 64; // Return variants for check_liquidatable method, should be wrapped in a Result // for a future possiblity of returning any error #[derive(PartialEq)] pub enum CheckLiquidatable { NotLiquidatable, Liquidatable, BecameNotLiquidatable, } pub struct MangoAccountPdaSeeds { pub group: Pubkey, pub owner: Pubkey, pub account_num_bytes: [u8; 4], pub bump_bytes: [u8; 1], } impl MangoAccountPdaSeeds { pub fn signer_seeds(&self) -> [&[u8]; 5] { [ b"MangoAccount".as_ref(), self.group.as_ref(), self.owner.as_ref(), &self.account_num_bytes, &self.bump_bytes, ] } } // Mango Account // This struct definition is only for clients e.g. typescript, so that they can easily use out of the box // deserialization and not have to do custom deserialization // On chain, we would prefer zero-copying to optimize for compute // // The MangoAccount binary data has changed over time: // - v1: The original version, many mainnet accounts still are this version. // The MangoAccount struct below describes v1 to make sure reading by IDL works for all live // accounts. // - v2: Introduced in v0.18.0 to add token conditional swaps at the end. Users using account // resizing before v0.20.0 would migrate to this version. // - v3: Introduced in v0.20.0 to add 64 zero bytes at the end for future expansion. // Users will migrate to this version when resizing their accounts. Also the // AccountSizeMigration instruction was used to bring all accounts to // this version after v0.20.0 was deployed. // // Version v0.22.0 drops idl support for v1 and v2 accounts by extending the MangoAccount idl with the // new fields. // // When not reading via idl, MangoAccount binary data is backwards compatible: when ignoring trailing bytes, // a v2 account can be read as a v1 account and a v3 account can be read as v1 or v2 etc. #[account] #[derive(Derivative, PartialEq)] #[derivative(Debug)] pub struct MangoAccount { // fixed // note: keep MangoAccountFixed in sync with changes here // ABI: Clients rely on this being at offset 8 pub group: Pubkey, // ABI: Clients rely on this being at offset 40 pub owner: Pubkey, #[derivative(Debug(format_with = "util::format_zero_terminated_utf8_bytes"))] pub name: [u8; 32], // Alternative authority/signer of transactions for a mango account pub delegate: Pubkey, pub account_num: u32, /// Tracks that this account should be liquidated until init_health >= 0. /// /// Normally accounts can not be liquidated while maint_health >= 0. But when an account /// reaches maint_health < 0, liquidators will call a liquidation instruction and thereby /// set this flag. Now the account may be liquidated until init_health >= 0. /// /// Many actions should be disabled while the account is being liquidated, even if /// its maint health has recovered to positive. Creating new open orders would, for example, /// confuse liquidators. pub being_liquidated: u8, /// The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd. /// /// Must never be set after a transaction ends. pub in_health_region: u8, pub bump: u8, #[derivative(Debug = "ignore")] pub padding: [u8; 1], // (Display only) // Cumulative (deposits - withdraws) // using USD prices at the time of the deposit/withdraw // in USD units with 6 decimals pub net_deposits: i64, // (Display only) // Cumulative transfers from perp to spot positions pub perp_spot_transfers: i64, /// Init health as calculated during HealthReginBegin, rounded up. pub health_region_begin_init_health: i64, pub frozen_until: u64, /// Fees usable with the "fees buyback" feature. /// This tracks the ones that accrued in the current expiry interval. pub buyback_fees_accrued_current: u64, /// Fees buyback amount from the previous expiry interval. pub buyback_fees_accrued_previous: u64, /// End timestamp of the current expiry interval of the buyback fees amount. pub buyback_fees_expiry_timestamp: u64, /// Next id to use when adding a token condition swap pub next_token_conditional_swap_id: u64, pub temporary_delegate: Pubkey, pub temporary_delegate_expiry: u64, /// Time at which the last collateral fee was charged pub last_collateral_fee_charge: u64, #[derivative(Debug = "ignore")] pub reserved: [u8; 152], // dynamic pub header_version: u8, #[derivative(Debug = "ignore")] pub padding3: [u8; 7], // note: padding is required for TokenPosition, etc. to be aligned #[derivative(Debug = "ignore")] pub padding4: u32, // Maps token_index -> deposit/borrow account for each token // that is active on this MangoAccount. pub tokens: Vec, #[derivative(Debug = "ignore")] pub padding5: u32, // Maps serum_market_index -> open orders for each serum market // that is active on this MangoAccount. pub serum3: Vec, #[derivative(Debug = "ignore")] pub padding6: u32, pub perps: Vec, #[derivative(Debug = "ignore")] pub padding7: u32, pub perp_open_orders: Vec, #[derivative(Debug = "ignore")] pub padding8: u32, pub token_conditional_swaps: Vec, #[derivative(Debug = "ignore")] pub reserved_dynamic: [u8; 64], } impl MangoAccount { pub fn default_for_tests() -> Self { Self { name: Default::default(), group: Pubkey::default(), owner: Pubkey::default(), delegate: Pubkey::default(), being_liquidated: 0, in_health_region: 0, account_num: 0, bump: 0, padding: Default::default(), net_deposits: 0, perp_spot_transfers: 0, health_region_begin_init_health: 0, frozen_until: 0, buyback_fees_accrued_current: 0, buyback_fees_accrued_previous: 0, buyback_fees_expiry_timestamp: 0, next_token_conditional_swap_id: 0, temporary_delegate: Pubkey::default(), temporary_delegate_expiry: 0, last_collateral_fee_charge: 0, reserved: [0; 152], header_version: DEFAULT_MANGO_ACCOUNT_VERSION, padding3: Default::default(), padding4: Default::default(), tokens: vec![TokenPosition::default(); 3], padding5: Default::default(), serum3: vec![Serum3Orders::default(); 5], padding6: Default::default(), perps: vec![PerpPosition::default(); 4], padding7: Default::default(), perp_open_orders: vec![PerpOpenOrder::default(); 6], padding8: Default::default(), token_conditional_swaps: vec![TokenConditionalSwap::default(); 2], reserved_dynamic: [0; 64], } } /// Number of bytes needed for the MangoAccount, including the discriminator pub fn space( token_count: u8, serum3_count: u8, perp_count: u8, perp_oo_count: u8, token_conditional_swap_count: u8, ) -> usize { 8 + size_of::() + Self::dynamic_size( token_count, serum3_count, perp_count, perp_oo_count, token_conditional_swap_count, ) } pub fn dynamic_token_vec_offset() -> usize { 8 // header version + padding + BORSH_VEC_PADDING_BYTES } pub fn dynamic_serum3_vec_offset(token_count: u8) -> usize { Self::dynamic_token_vec_offset() + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(token_count)) + BORSH_VEC_PADDING_BYTES } pub fn dynamic_perp_vec_offset(token_count: u8, serum3_count: u8) -> usize { Self::dynamic_serum3_vec_offset(token_count) + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(serum3_count)) + BORSH_VEC_PADDING_BYTES } pub fn dynamic_perp_oo_vec_offset(token_count: u8, serum3_count: u8, perp_count: u8) -> usize { Self::dynamic_perp_vec_offset(token_count, serum3_count) + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(perp_count)) + BORSH_VEC_PADDING_BYTES } pub fn dynamic_token_conditional_swap_vec_offset( token_count: u8, serum3_count: u8, perp_count: u8, perp_oo_count: u8, ) -> usize { Self::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count) + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(perp_oo_count)) + BORSH_VEC_PADDING_BYTES } pub fn dynamic_reserved_bytes_offset( token_count: u8, serum3_count: u8, perp_count: u8, perp_oo_count: u8, token_conditional_swap_count: u8, ) -> usize { Self::dynamic_token_conditional_swap_vec_offset( token_count, serum3_count, perp_count, perp_oo_count, ) + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(token_conditional_swap_count)) } pub fn dynamic_size( token_count: u8, serum3_count: u8, perp_count: u8, perp_oo_count: u8, token_conditional_swap_count: u8, ) -> usize { Self::dynamic_reserved_bytes_offset( token_count, serum3_count, perp_count, perp_oo_count, token_conditional_swap_count, ) + DYNAMIC_RESERVED_BYTES } } // Mango Account fixed part for easy zero copy deserialization #[zero_copy] pub struct MangoAccountFixed { pub group: Pubkey, pub owner: Pubkey, pub name: [u8; 32], pub delegate: Pubkey, pub account_num: u32, being_liquidated: u8, in_health_region: u8, pub bump: u8, pub padding: [u8; 1], pub net_deposits: i64, pub perp_spot_transfers: i64, pub health_region_begin_init_health: i64, pub frozen_until: u64, pub buyback_fees_accrued_current: u64, pub buyback_fees_accrued_previous: u64, pub buyback_fees_expiry_timestamp: u64, pub next_token_conditional_swap_id: u64, pub temporary_delegate: Pubkey, pub temporary_delegate_expiry: u64, pub last_collateral_fee_charge: u64, pub reserved: [u8; 152], } const_assert_eq!( size_of::(), 32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152 ); const_assert_eq!(size_of::(), 400); const_assert_eq!(size_of::() % 8, 0); impl MangoAccountFixed { pub fn name(&self) -> &str { std::str::from_utf8(&self.name) .unwrap() .trim_matches(char::from(0)) } pub fn is_operational(&self) -> bool { let now_ts: u64 = Clock::get().unwrap().unix_timestamp.try_into().unwrap(); self.frozen_until < now_ts } pub fn is_owner_or_delegate(&self, ix_signer: Pubkey) -> bool { self.owner == ix_signer || self.is_delegate(ix_signer) } pub fn is_delegate(&self, ix_signer: Pubkey) -> bool { if self.delegate == ix_signer { return true; } let now_ts: u64 = Clock::get().unwrap().unix_timestamp.try_into().unwrap(); if now_ts > self.temporary_delegate_expiry { return false; } self.temporary_delegate == ix_signer } pub fn being_liquidated(&self) -> bool { self.being_liquidated == 1 } pub fn set_being_liquidated(&mut self, b: bool) { self.being_liquidated = u8::from(b); } pub fn is_in_health_region(&self) -> bool { self.in_health_region == 1 } pub fn set_in_health_region(&mut self, b: bool) { self.in_health_region = u8::from(b); } pub fn maybe_recover_from_being_liquidated(&mut self, liq_end_health: I80F48) -> bool { // This is used as threshold to flip flag instead of 0 because of dust issues let one_native_usdc = I80F48::ONE; if self.being_liquidated() && liq_end_health > -one_native_usdc { self.set_being_liquidated(false); true } else { false } } /// Updates the buyback_fees_* fields for staggered expiry of available amounts. pub fn expire_buyback_fees(&mut self, now_ts: u64, interval: u64) { if interval == 0 || now_ts < self.buyback_fees_expiry_timestamp { return; } else if now_ts < self.buyback_fees_expiry_timestamp + interval { self.buyback_fees_accrued_previous = self.buyback_fees_accrued_current; } else { self.buyback_fees_accrued_previous = 0; } self.buyback_fees_accrued_current = 0; self.buyback_fees_expiry_timestamp = (now_ts / interval + 1) * interval; } /// The total buyback fees amount that the account can make use of. pub fn buyback_fees_accrued(&self) -> u64 { self.buyback_fees_accrued_current .saturating_add(self.buyback_fees_accrued_previous) } /// Add new fees that are usable with the buyback fees feature. /// /// Any call to this should be preceeded by a call to expire_buyback_fees earlier /// in the same instruction. pub fn accrue_buyback_fees(&mut self, amount: u64) { self.buyback_fees_accrued_current = self.buyback_fees_accrued_current.saturating_add(amount); } /// Reduce the available buyback fees amount because it was used up. /// /// Panics if `amount` exceeds the available accrued amount pub fn reduce_buyback_fees_accrued(&mut self, amount: u64) { if amount > self.buyback_fees_accrued_previous { let remaining_amount = amount - self.buyback_fees_accrued_previous; assert!(remaining_amount <= self.buyback_fees_accrued_current); self.buyback_fees_accrued_current -= remaining_amount; self.buyback_fees_accrued_previous = 0; } else { self.buyback_fees_accrued_previous -= amount; } } pub fn pda_seeds(&self) -> MangoAccountPdaSeeds { MangoAccountPdaSeeds { group: self.group, owner: self.owner, account_num_bytes: self.account_num.to_le_bytes(), bump_bytes: [self.bump], } } } impl Owner for MangoAccountFixed { fn owner() -> Pubkey { MangoAccount::owner() } } impl Discriminator for MangoAccountFixed { const DISCRIMINATOR: [u8; 8] = MangoAccount::DISCRIMINATOR; } impl anchor_lang::ZeroCopy for MangoAccountFixed {} #[derive(Clone, Debug)] pub struct MangoAccountDynamicHeader { pub token_count: u8, pub serum3_count: u8, pub perp_count: u8, pub perp_oo_count: u8, pub token_conditional_swap_count: u8, } impl DynamicHeader for MangoAccountDynamicHeader { 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![ dynamic_data, MangoAccount::dynamic_token_vec_offset(), BORSH_VEC_SIZE_BYTES ])) .unwrap(); let serum3_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ 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![ 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![ dynamic_data, MangoAccount::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count), BORSH_VEC_SIZE_BYTES ])) .unwrap(); let token_conditional_swap_vec_offset = MangoAccount::dynamic_token_conditional_swap_vec_offset( token_count, serum3_count, perp_count, perp_oo_count, ); let token_conditional_swap_count = if dynamic_data.len() > token_conditional_swap_vec_offset + BORSH_VEC_SIZE_BYTES { u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ dynamic_data, token_conditional_swap_vec_offset, BORSH_VEC_SIZE_BYTES ])) .unwrap() } else { 0 }; Ok(Self { token_count, serum3_count, perp_count, perp_oo_count, token_conditional_swap_count, }) } _ => err!(MangoError::NotImplementedError).context("unexpected header version number"), } } 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(()) } } fn get_helper(data: &[u8], index: usize) -> &T { bytemuck::from_bytes(&data[index..index + size_of::()]) } fn get_helper_mut(data: &mut [u8], index: usize) -> &mut T { bytemuck::from_bytes_mut(&mut data[index..index + size_of::()]) } impl MangoAccountDynamicHeader { pub fn account_size(&self) -> usize { MangoAccount::space( self.token_count, self.serum3_count, self.perp_count, self.perp_oo_count, self.token_conditional_swap_count, ) } // offset into dynamic data where 1st TokenPosition would be found // todo make fn private pub fn token_offset(&self, raw_index: usize) -> usize { MangoAccount::dynamic_token_vec_offset() + BORSH_VEC_SIZE_BYTES + raw_index * size_of::() } // offset into dynamic data where 1st Serum3Orders would be found // todo make fn private pub fn serum3_offset(&self, raw_index: usize) -> usize { MangoAccount::dynamic_serum3_vec_offset(self.token_count) + BORSH_VEC_SIZE_BYTES + raw_index * size_of::() } // offset into dynamic data where 1st PerpPosition would be found pub fn perp_offset(&self, raw_index: usize) -> usize { MangoAccount::dynamic_perp_vec_offset(self.token_count, self.serum3_count) + BORSH_VEC_SIZE_BYTES + raw_index * size_of::() } fn perp_oo_offset(&self, raw_index: usize) -> usize { MangoAccount::dynamic_perp_oo_vec_offset( self.token_count, self.serum3_count, self.perp_count, ) + BORSH_VEC_SIZE_BYTES + raw_index * size_of::() } fn token_conditional_swap_offset(&self, raw_index: usize) -> usize { MangoAccount::dynamic_token_conditional_swap_vec_offset( self.token_count, self.serum3_count, self.perp_count, self.perp_oo_count, ) + BORSH_VEC_SIZE_BYTES + raw_index * size_of::() } fn reserved_bytes_offset(&self) -> usize { MangoAccount::dynamic_reserved_bytes_offset( self.token_count, self.serum3_count, self.perp_count, self.perp_oo_count, self.token_conditional_swap_count, ) } pub fn token_count(&self) -> usize { self.token_count.into() } pub fn serum3_count(&self) -> usize { self.serum3_count.into() } pub fn perp_count(&self) -> usize { self.perp_count.into() } pub fn perp_oo_count(&self) -> usize { self.perp_oo_count.into() } pub fn token_conditional_swap_count(&self) -> usize { self.token_conditional_swap_count.into() } pub fn zero() -> Self { Self { token_count: 0, serum3_count: 0, perp_count: 0, perp_oo_count: 0, token_conditional_swap_count: 0, } } pub fn expected_health_accounts(&self) -> usize { self.token_count() * 2 + self.serum3_count() + self.perp_count() * 2 } pub fn max_health_accounts() -> usize { 28 } /// Error if this header isn't a valid resize from `prev` /// /// - Check that the total health accounts stay limited /// (this coverers token, perp, serum position limits) /// - Check that if perp oo/tcs size increases, it is bounded by the limits /// - If a field doesn't change, don't error if it exceeds the limits /// (might have been expanded earlier when it was valid to do) pub fn check_resize_from(&self, prev: &Self) -> Result<()> { let new_health_accounts = self.expected_health_accounts(); let prev_health_accounts = prev.expected_health_accounts(); if new_health_accounts > prev_health_accounts { require_gte!(Self::max_health_accounts(), new_health_accounts); } if self.perp_oo_count > prev.perp_oo_count { require_gte!(64, self.perp_oo_count); } if self.token_conditional_swap_count > prev.token_conditional_swap_count { require_gte!(64, self.token_conditional_swap_count); } Ok(()) } } /// 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 pub fn from_bytes(bytes: &[u8]) -> Result { let (fixed, dynamic) = bytes.split_at(size_of::()); Ok(Self { fixed: *bytemuck::from_bytes(fixed), header: MangoAccountDynamicHeader::from_bytes(dynamic)?, dynamic: dynamic.to_vec(), }) } } impl<'a> MangoAccountLoadedRef<'a> { // bytes without discriminator pub fn from_bytes(bytes: &'a [u8]) -> Result { let (fixed, dynamic) = bytes.split_at(size_of::()); Ok(Self { fixed: bytemuck::from_bytes(fixed), header: MangoAccountDynamicHeader::from_bytes(dynamic)?, dynamic, }) } } // This generic impl covers MangoAccountRef, MangoAccountRefMut and other // DynamicAccount variants that allow read access. impl< Header: DerefOrBorrow, Fixed: DerefOrBorrow, Dynamic: DerefOrBorrow<[u8]>, > DynamicAccount { fn header(&self) -> &MangoAccountDynamicHeader { self.header.deref_or_borrow() } pub fn header_version(&self) -> &u8 { get_helper(self.dynamic(), 0) } fn fixed(&self) -> &MangoAccountFixed { self.fixed.deref_or_borrow() } fn dynamic(&self) -> &[u8] { self.dynamic.deref_or_borrow() } #[allow(dead_code)] fn dynamic_reserved_bytes(&self) -> &[u8] { let reserved_offset = self.header().reserved_bytes_offset(); &self.dynamic()[reserved_offset..reserved_offset + DYNAMIC_RESERVED_BYTES] } /// Returns /// - the position /// - the raw index into the token positions list (for use with get_raw/deactivate) pub fn token_position_and_raw_index( &self, token_index: TokenIndex, ) -> Result<(&TokenPosition, usize)> { self.all_token_positions() .enumerate() .find_map(|(raw_index, p)| p.is_active_for_token(token_index).then_some((p, raw_index))) .ok_or_else(|| { error_msg_typed!( MangoError::TokenPositionDoesNotExist, "position for token index {} not found", token_index ) }) } pub fn token_position(&self, token_index: TokenIndex) -> Result<&TokenPosition> { self.token_position_and_raw_index(token_index) .map(|(p, _)| p) } pub(crate) fn token_position_by_raw_index_unchecked(&self, raw_index: usize) -> &TokenPosition { get_helper(self.dynamic(), self.header().token_offset(raw_index)) } pub fn token_position_by_raw_index(&self, raw_index: usize) -> Result<&TokenPosition> { require_gt!(self.header().token_count(), raw_index); Ok(self.token_position_by_raw_index_unchecked(raw_index)) } // get iter over all TokenPositions (including inactive) pub fn all_token_positions(&self) -> impl Iterator + '_ { (0..self.header().token_count()).map(|i| self.token_position_by_raw_index_unchecked(i)) } // get iter over all active TokenPositions pub fn active_token_positions(&self) -> impl Iterator + '_ { self.all_token_positions().filter(|token| token.is_active()) } pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Result<&Serum3Orders> { self.all_serum3_orders() .find(|p| p.is_active_for_market(market_index)) .ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index)) } pub(crate) fn serum3_orders_by_raw_index_unchecked(&self, raw_index: usize) -> &Serum3Orders { get_helper(self.dynamic(), self.header().serum3_offset(raw_index)) } pub fn serum3_orders_by_raw_index(&self, raw_index: usize) -> Result<&Serum3Orders> { require_gt!(self.header().serum3_count(), raw_index); Ok(self.serum3_orders_by_raw_index_unchecked(raw_index)) } pub fn all_serum3_orders(&self) -> impl Iterator + '_ { (0..self.header().serum3_count()).map(|i| self.serum3_orders_by_raw_index_unchecked(i)) } pub fn active_serum3_orders(&self) -> impl Iterator + '_ { self.all_serum3_orders() .filter(|serum3_order| serum3_order.is_active()) } pub fn perp_position(&self, market_index: PerpMarketIndex) -> Result<&PerpPosition> { self.all_perp_positions() .find(|p| p.is_active_for_market(market_index)) .ok_or_else(|| error!(MangoError::PerpPositionDoesNotExist)) } pub(crate) fn perp_position_by_raw_index_unchecked(&self, raw_index: usize) -> &PerpPosition { get_helper(self.dynamic(), self.header().perp_offset(raw_index)) } pub fn perp_position_by_raw_index(&self, raw_index: usize) -> Result<&PerpPosition> { require_gt!(self.header().perp_count(), raw_index); Ok(self.perp_position_by_raw_index_unchecked(raw_index)) } pub fn all_perp_positions(&self) -> impl Iterator { (0..self.header().perp_count()).map(|i| self.perp_position_by_raw_index_unchecked(i)) } pub fn active_perp_positions(&self) -> impl Iterator { self.all_perp_positions().filter(|p| p.is_active()) } pub(crate) fn perp_order_by_raw_index_unchecked(&self, raw_index: usize) -> &PerpOpenOrder { get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index)) } pub fn perp_order_by_raw_index(&self, raw_index: usize) -> Result<&PerpOpenOrder> { require_gt!(self.header().perp_oo_count(), raw_index); Ok(self.perp_order_by_raw_index_unchecked(raw_index)) } pub fn all_perp_orders(&self) -> impl Iterator { (0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index_unchecked(i)) } pub fn perp_next_order_slot(&self) -> Result { self.all_perp_orders() .position(|&oo| oo.market == FREE_ORDER_SLOT) .ok_or_else(|| error_msg!("no free perp order index")) } pub fn perp_find_order_with_client_order_id( &self, market_index: PerpMarketIndex, client_order_id: u64, ) -> Option<(usize, &PerpOpenOrder)> { self.all_perp_orders().enumerate().find(|(_, &oo)| { oo.is_active_for_market(market_index) && oo.client_id == client_order_id }) } pub fn perp_find_order_with_order_id( &self, market_index: PerpMarketIndex, order_id: u128, ) -> Option<(usize, &PerpOpenOrder)> { self.all_perp_orders() .enumerate() .find(|(_, &oo)| oo.is_active_for_market(market_index) && oo.id == order_id) } pub fn being_liquidated(&self) -> bool { self.fixed().being_liquidated() } fn token_conditional_swap_by_index_unchecked(&self, index: usize) -> &TokenConditionalSwap { get_helper( self.dynamic(), self.header().token_conditional_swap_offset(index), ) } pub fn token_conditional_swap_by_index(&self, index: usize) -> Result<&TokenConditionalSwap> { require_gt!(self.header().token_conditional_swap_count(), index); Ok(self.token_conditional_swap_by_index_unchecked(index)) } pub fn token_conditional_swap_by_id(&self, id: u64) -> Result<(usize, &TokenConditionalSwap)> { let index = self .all_token_conditional_swaps() .position(|tcs| tcs.is_configured() && tcs.id == id) .ok_or_else(|| error_msg!("token conditional swap with id {} not found", id))?; Ok((index, self.token_conditional_swap_by_index_unchecked(index))) } pub fn all_token_conditional_swaps(&self) -> impl Iterator { (0..self.header().token_conditional_swap_count()) .map(|i| self.token_conditional_swap_by_index_unchecked(i)) } pub fn active_token_conditional_swaps(&self) -> impl Iterator { self.all_token_conditional_swaps() .filter(|p| p.is_configured()) } pub fn token_conditional_swap_free_index(&self) -> Result { self.all_token_conditional_swaps() .position(|&v| !v.is_configured()) .ok_or_else(|| error_msg!("no free token conditional swap index")) } pub fn borrow(&self) -> MangoAccountRef { MangoAccountRef { header: self.header(), fixed: self.fixed(), dynamic: self.dynamic(), } } } impl< Header: DerefOrBorrowMut + DerefOrBorrow, Fixed: DerefOrBorrowMut + DerefOrBorrow, Dynamic: DerefOrBorrowMut<[u8]> + DerefOrBorrow<[u8]>, > DynamicAccount { fn header_mut(&mut self) -> &mut MangoAccountDynamicHeader { self.header.deref_or_borrow_mut() } fn fixed_mut(&mut self) -> &mut MangoAccountFixed { self.fixed.deref_or_borrow_mut() } fn dynamic_mut(&mut self) -> &mut [u8] { self.dynamic.deref_or_borrow_mut() } 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(), } } /// Returns /// - the position /// - the raw index into the token positions list (for use with get_raw/deactivate) pub fn token_position_mut( &mut self, token_index: TokenIndex, ) -> Result<(&mut TokenPosition, usize)> { let raw_index = self .all_token_positions() .enumerate() .find_map(|(raw_index, p)| p.is_active_for_token(token_index).then_some(raw_index)) .ok_or_else(|| { error_msg_typed!( MangoError::TokenPositionDoesNotExist, "position for token index {} not found", token_index ) })?; Ok((self.token_position_mut_by_raw_index(raw_index), raw_index)) } // get mut TokenPosition at raw_index pub fn token_position_mut_by_raw_index(&mut self, raw_index: usize) -> &mut TokenPosition { let offset = self.header().token_offset(raw_index); get_helper_mut(self.dynamic_mut(), offset) } /// Creates or retrieves a TokenPosition for the token_index. /// Returns: /// - the position /// - the raw index into the token positions list (for use with get_raw) /// - the active index, for use with FixedOrderAccountRetriever pub fn ensure_token_position( &mut self, token_index: TokenIndex, ) -> Result<(&mut TokenPosition, usize, usize)> { let mut active_index = 0; let mut match_or_free = None; for (raw_index, position) in self.all_token_positions().enumerate() { if position.is_active_for_token(token_index) { // Can't return early because of lifetimes match_or_free = Some((raw_index, active_index)); break; } if position.is_active() { active_index += 1; } else if match_or_free.is_none() { match_or_free = Some((raw_index, active_index)); } } if let Some((raw_index, bank_index)) = match_or_free { let v = self.token_position_mut_by_raw_index(raw_index); if !v.is_active_for_token(token_index) { *v = TokenPosition { indexed_position: I80F48::ZERO, token_index, in_use_count: 0, cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, padding: Default::default(), reserved: [0; 128], }; } Ok((v, raw_index, bank_index)) } else { err!(MangoError::NoFreeTokenPositionIndex) .context(format!("when looking for token index {}", token_index)) } } pub fn deactivate_token_position(&mut self, raw_index: usize) { assert!(self.token_position_mut_by_raw_index(raw_index).in_use_count == 0); self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX; } pub fn deactivate_token_position_and_log( &mut self, raw_index: usize, mango_account_pubkey: Pubkey, ) { let mango_group = self.fixed().group; let token_position = self.token_position_mut_by_raw_index(raw_index); assert!(token_position.in_use_count == 0); emit_stack(DeactivateTokenPositionLog { mango_group, mango_account: mango_account_pubkey, token_index: token_position.token_index, cumulative_deposit_interest: token_position.cumulative_deposit_interest, cumulative_borrow_interest: token_position.cumulative_borrow_interest, }); self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX; } /// Decrements the in_use_count for the token position for the bank. /// /// If it goes to 0, the position may be dusted (if between 0 and 1 native tokens) /// and closed. pub fn token_decrement_dust_deactivate( &mut self, bank: &mut crate::state::Bank, now_ts: u64, mango_account_pubkey: Pubkey, ) -> Result<()> { let token_result = self.token_position_mut(bank.token_index); if token_result.is_anchor_error_with_code(MangoError::TokenPositionDoesNotExist.into()) { // Already deactivated is ok return Ok(()); } let (position, raw_index) = token_result?; position.decrement_in_use(); let active = bank.dust_if_possible(position, now_ts)?; if !active { self.deactivate_token_position_and_log(raw_index, mango_account_pubkey); } Ok(()) } // get mut Serum3Orders at raw_index pub fn serum3_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut Serum3Orders { let offset = self.header().serum3_offset(raw_index); get_helper_mut(self.dynamic_mut(), offset) } pub fn create_serum3_orders( &mut self, market_index: Serum3MarketIndex, ) -> Result<&mut Serum3Orders> { if self.serum3_orders(market_index).is_ok() { return err!(MangoError::Serum3OpenOrdersExistAlready); } let raw_index_opt = self.all_serum3_orders().position(|p| !p.is_active()); if let Some(raw_index) = raw_index_opt { *(self.serum3_orders_mut_by_raw_index(raw_index)) = Serum3Orders { market_index: market_index as Serum3MarketIndex, ..Serum3Orders::default() }; Ok(self.serum3_orders_mut_by_raw_index(raw_index)) } else { err!(MangoError::NoFreeSerum3OpenOrdersIndex) } } pub fn deactivate_serum3_orders(&mut self, market_index: Serum3MarketIndex) -> Result<()> { let raw_index = self .all_serum3_orders() .position(|p| p.is_active_for_market(market_index)) .ok_or_else(|| error_msg!("serum3 open orders index {} not found", market_index))?; self.serum3_orders_mut_by_raw_index(raw_index).market_index = Serum3MarketIndex::MAX; Ok(()) } pub fn serum3_orders_mut( &mut self, market_index: Serum3MarketIndex, ) -> Result<&mut Serum3Orders> { let raw_index_opt = self .all_serum3_orders() .position(|p| p.is_active_for_market(market_index)); raw_index_opt .map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index)) .ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index)) } // get mut PerpPosition at raw_index pub fn perp_position_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpPosition { let offset = self.header().perp_offset(raw_index); get_helper_mut(self.dynamic_mut(), offset) } pub fn perp_order_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrder { let offset = self.header().perp_oo_offset(raw_index); get_helper_mut(self.dynamic_mut(), offset) } pub fn perp_position_mut( &mut self, market_index: PerpMarketIndex, ) -> Result<&mut PerpPosition> { let raw_index_opt = self .all_perp_positions() .position(|p| p.is_active_for_market(market_index)); raw_index_opt .map(|raw_index| self.perp_position_mut_by_raw_index(raw_index)) .ok_or_else(|| error!(MangoError::PerpPositionDoesNotExist)) } pub fn ensure_perp_position( &mut self, perp_market_index: PerpMarketIndex, settle_token_index: TokenIndex, ) -> Result<(&mut PerpPosition, usize)> { let mut raw_index_opt = self .all_perp_positions() .position(|p| p.is_active_for_market(perp_market_index)); if raw_index_opt.is_none() { raw_index_opt = self.all_perp_positions().position(|p| !p.is_active()); if let Some(raw_index) = raw_index_opt { let perp_position = self.perp_position_mut_by_raw_index(raw_index); *perp_position = PerpPosition::default(); perp_position.market_index = perp_market_index; let settle_token_position = self.ensure_token_position(settle_token_index)?.0; settle_token_position.increment_in_use(); } } if let Some(raw_index) = raw_index_opt { Ok((self.perp_position_mut_by_raw_index(raw_index), raw_index)) } else { err!(MangoError::NoFreePerpPositionIndex) } } // Only used in unit tests pub fn deactivate_perp_position( &mut self, perp_market_index: PerpMarketIndex, settle_token_index: TokenIndex, ) -> Result<()> { self.perp_position_mut(perp_market_index)?.market_index = PerpMarketIndex::MAX; let settle_token_position = self.token_position_mut(settle_token_index)?.0; settle_token_position.decrement_in_use(); Ok(()) } pub fn deactivate_perp_position_and_log( &mut self, perp_market_index: PerpMarketIndex, settle_token_index: TokenIndex, mango_account_pubkey: Pubkey, ) -> Result<()> { let mango_group = self.fixed().group; let perp_position = self.perp_position_mut(perp_market_index)?; emit_stack(DeactivatePerpPositionLog { mango_group, mango_account: mango_account_pubkey, market_index: perp_market_index, cumulative_long_funding: perp_position.cumulative_long_funding, cumulative_short_funding: perp_position.cumulative_short_funding, maker_volume: perp_position.maker_volume, taker_volume: perp_position.taker_volume, perp_spot_transfers: perp_position.perp_spot_transfers, }); perp_position.market_index = PerpMarketIndex::MAX; let settle_token_position = self.token_position_mut(settle_token_index)?.0; settle_token_position.decrement_in_use(); Ok(()) } pub fn find_first_active_unused_perp_position(&self) -> Option<&PerpPosition> { let first_unused_position_opt = self.all_perp_positions().find(|p| { p.is_active() && p.base_position_lots == 0 && p.quote_position_native == 0 && p.bids_base_lots == 0 && p.asks_base_lots == 0 && p.taker_base_lots == 0 && p.taker_quote_lots == 0 }); first_unused_position_opt } pub fn add_perp_order( &mut self, perp_market_index: PerpMarketIndex, side: Side, order_tree: BookSideOrderTree, order: &LeafNode, ) -> Result<()> { let perp_account = self.perp_position_mut(perp_market_index)?; perp_account.adjust_maker_lots(side, order.quantity); let slot = order.owner_slot as usize; let oo = self.perp_order_mut_by_raw_index(slot); oo.market = perp_market_index; oo.side_and_tree = SideAndOrderTree::new(side, order_tree).into(); oo.id = order.key; oo.client_id = order.client_order_id; oo.quantity = order.quantity; Ok(()) } /// Removes the perp order and updates the maker bids/asks tracking /// /// The passed in `quantity` may differ from the quantity stored on the /// perp open order slot, because maybe we're cancelling an order slot /// for quantity 10 where 3 are in-flight in a FillEvent and 7 were left /// on the book. pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> { let oo = self.perp_order_by_raw_index(slot)?; require_neq!(oo.market, FREE_ORDER_SLOT); let perp_market_index = oo.market; let order_side = oo.side_and_tree().side(); let perp_account = self.perp_position_mut(perp_market_index)?; perp_account.adjust_maker_lots(order_side, -quantity); let oo = self.perp_order_mut_by_raw_index(slot); oo.clear(); Ok(()) } /// Returns amount of realized trade pnl for the maker pub fn execute_perp_maker( &mut self, perp_market_index: PerpMarketIndex, perp_market: &mut PerpMarket, fill: &FillEvent, group: &Group, ) -> Result { let side = fill.taker_side().invert_side(); let (base_change, quote_change) = fill.base_quote_change(side); let quote = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); let fees = quote.abs() * I80F48::from_num(fill.maker_fee); if fees.is_positive() { let f = self.fixed_mut(); let now_ts = Clock::get().unwrap().unix_timestamp.try_into().unwrap(); f.expire_buyback_fees(now_ts, group.buyback_fees_expiry_interval); f.accrue_buyback_fees(fees.floor().to_num::()); } let pa = self.perp_position_mut(perp_market_index)?; pa.settle_funding(perp_market); pa.record_trading_fee(fees); let realized_pnl = pa.record_trade(perp_market, base_change, quote); pa.maker_volume += quote.abs().to_num::(); let quantity_filled = base_change.abs(); let maker_slot = fill.maker_slot as usize; // Always adjust the bids/asks_base_lots for the filled amount. // Because any early cancels only adjust it for the amount that was on the book, // so even fill events that come after the slot was freed still need to clear // the pending maker lots. pa.adjust_maker_lots(side, -quantity_filled); let oo = self.perp_order_mut_by_raw_index(maker_slot); let is_active = oo.is_active_for_market(perp_market_index); // Old fill events have no maker order id and match against any order. // (this works safely because we don't allow old order's slots to be // prematurely freed - and new orders can only have new fill events) let is_old_fill = fill.maker_order_id == 0; let order_id_match = is_old_fill || oo.id == fill.maker_order_id; if is_active && order_id_match { // Old orders have quantity=0 oo.quantity = (oo.quantity - quantity_filled).max(0); if fill.maker_out() { oo.clear(); } } Ok(realized_pnl) } /// Returns amount of realized trade pnl for the taker pub fn execute_perp_taker( &mut self, perp_market_index: PerpMarketIndex, perp_market: &mut PerpMarket, fill: &FillEvent, ) -> Result { let pa = self.perp_position_mut(perp_market_index)?; pa.settle_funding(perp_market); let (base_change, quote_change) = fill.base_quote_change(fill.taker_side()); pa.remove_taker_trade(base_change, quote_change); // fees are assessed at time of trade; no need to assess fees here let quote_change_native = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); let realized_pnl = pa.record_trade(perp_market, base_change, quote_change_native); pa.taker_volume += quote_change_native.abs().to_num::(); Ok(realized_pnl) } pub fn execute_perp_out_event( &mut self, perp_market_index: PerpMarketIndex, side: Side, slot: usize, quantity: i64, order_id: u128, ) -> Result<()> { // Always free up the maker lots tracking, regardless of whether the // order slot is still on the account or not let pa = self.perp_position_mut(perp_market_index)?; pa.adjust_maker_lots(side, -quantity); let oo = self.perp_order_mut_by_raw_index(slot); let is_active = oo.is_active_for_market(perp_market_index); // Old events have no order id and match against any order. // (this works safely because we don't allow old order's slots to be // prematurely freed - and new orders can only have new events) let is_old_event = order_id == 0; let order_id_match = is_old_event || oo.id == order_id; // This may be a delayed out event (slot may be empty or reused), so make // sure it's the right one before canceling. if is_active && order_id_match { oo.clear(); } Ok(()) } pub fn token_conditional_swap_mut_by_index( &mut self, index: usize, ) -> Result<&mut TokenConditionalSwap> { let count: usize = self.header().token_conditional_swap_count.into(); require_gt!(count, index); let offset = self.header().token_conditional_swap_offset(index); Ok(get_helper_mut(self.dynamic_mut(), offset)) } pub fn free_token_conditional_swap_mut(&mut self) -> Result<&mut TokenConditionalSwap> { let index = self.token_conditional_swap_free_index()?; let tcs = self.token_conditional_swap_mut_by_index(index)?; Ok(tcs) } pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result { let pre_init_health = health_cache.health(HealthType::Init); msg!("pre_init_health: {}", pre_init_health); self.check_health_pre_checks(health_cache, pre_init_health)?; Ok(pre_init_health) } pub fn check_health_pre_checks( &mut self, health_cache: &HealthCache, pre_init_health: I80F48, ) -> Result<()> { // We can skip computing LiquidationEnd health if Init health > 0, because // LiquidationEnd health >= Init health. self.fixed_mut() .maybe_recover_from_being_liquidated(pre_init_health); if self.fixed().being_liquidated() { let liq_end_health = health_cache.health(HealthType::LiquidationEnd); self.fixed_mut() .maybe_recover_from_being_liquidated(liq_end_health); } require!( !self.fixed().being_liquidated(), MangoError::BeingLiquidated ); Ok(()) } pub fn check_health_post( &mut self, health_cache: &HealthCache, pre_init_health: I80F48, ) -> Result { let post_init_health = health_cache.health(HealthType::Init); msg!("post_init_health: {}", post_init_health); self.check_health_post_checks(pre_init_health, post_init_health)?; Ok(post_init_health) } pub fn check_health_post_checks( &mut self, pre_init_health: I80F48, post_init_health: I80F48, ) -> Result<()> { // Accounts that have negative init health may only take actions that don't further // decrease their health. // To avoid issues with rounding, we allow accounts to decrease their health by up to // $1e-6. This is safe because the grace amount is way less than the cost of a transaction. // And worst case, users can only use this to gradually drive their own account into // liquidation. // There is an exception for accounts with health between $0 and -$0.001 (-1000 native), // because we don't want to allow empty accounts or accounts with extremely tiny deposits // to immediately drive themselves into bankruptcy. (accounts with large deposits can also // be in this health range, but it's really unlikely) let health_does_not_decrease = if post_init_health < -1000 { post_init_health.ceil() >= pre_init_health.ceil() } else { post_init_health >= pre_init_health }; require!( post_init_health >= 0 || health_does_not_decrease, MangoError::HealthMustBePositiveOrIncrease ); Ok(()) } pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result { // Once maint_health falls below 0, we want to start liquidating, // we want to allow liquidation to continue until init_health is positive, // to prevent constant oscillation between the two states if self.being_liquidated() { let liq_end_health = health_cache.health(HealthType::LiquidationEnd); if self .fixed_mut() .maybe_recover_from_being_liquidated(liq_end_health) { msg!("Liqee init_health above zero"); return Ok(CheckLiquidatable::BecameNotLiquidatable); } } else { let maint_health = health_cache.health(HealthType::Maint); if maint_health >= I80F48::ZERO { msg!("Liqee is not liquidatable"); return Ok(CheckLiquidatable::NotLiquidatable); } self.fixed_mut().set_being_liquidated(true); } return Ok(CheckLiquidatable::Liquidatable); } fn write_borsh_vec_length_and_padding(&mut self, offset: usize, count: u8) { let dst: &mut [u8] = &mut self.dynamic_mut()[offset - BORSH_VEC_SIZE_BYTES - BORSH_VEC_PADDING_BYTES ..offset - BORSH_VEC_SIZE_BYTES]; dst.copy_from_slice(&[0u8; BORSH_VEC_PADDING_BYTES]); let dst: &mut [u8] = &mut self.dynamic_mut()[offset - BORSH_VEC_SIZE_BYTES..offset]; dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); } // writes length of tokens vec at appropriate offset so that borsh can infer the vector length // length used is that present in the header fn write_token_length(&mut self) { let offset = self.header().token_offset(0); let count = self.header().token_count; self.write_borsh_vec_length_and_padding(offset, count) } fn write_serum3_length(&mut self) { let offset = self.header().serum3_offset(0); let count = self.header().serum3_count; self.write_borsh_vec_length_and_padding(offset, count) } fn write_perp_length(&mut self) { let offset = self.header().perp_offset(0); let count = self.header().perp_count; self.write_borsh_vec_length_and_padding(offset, count) } fn write_perp_oo_length(&mut self) { let offset = self.header().perp_oo_offset(0); let count = self.header().perp_oo_count; self.write_borsh_vec_length_and_padding(offset, count) } fn write_token_conditional_swap_length(&mut self) { let offset = self.header().token_conditional_swap_offset(0); let count = self.header().token_conditional_swap_count; self.write_borsh_vec_length_and_padding(offset, count) } pub fn resize_dynamic_content( &mut self, new_token_count: u8, new_serum3_count: u8, new_perp_count: u8, new_perp_oo_count: u8, new_token_conditional_swap_count: u8, ) -> Result<()> { let new_header = MangoAccountDynamicHeader { token_count: new_token_count, serum3_count: new_serum3_count, perp_count: new_perp_count, perp_oo_count: new_perp_oo_count, token_conditional_swap_count: new_token_conditional_swap_count, }; let old_header = self.header().clone(); new_header.check_resize_from(&old_header)?; let dynamic = self.dynamic_mut(); // Resizing needs to move the existing bytes in `dynamic` around, preserving // existing data, possibly creating new entries or removing unused slots. // // The operation has four steps: // - Defrag: Move all active slots to the front. If a user's token slots were // (unused, token pos for 4, unused, token pos for 500, unused) // before, they'd be // (token pos for 4, token pos for 500, garbage, garbage, garbage) // after. That way all data that needs to be preserved for each type of // slot is one contiguous block. // - Moving preserved blocks to the left where needed, iterating blocks left to right. // - Moving preserved blocks to the right where needed, iterating blocks right to left. // - Default-initializing all non-preserved spaces. // "Defrag" token, serum, perp by moving active positions into the front slots // // Dangerous because this does NOT reset the previous positions! // Use the active_* values to know how many slots are in-use afterwards! // // Perp OOs can't be collapsed this way because LeafNode::owner_slot is an index into it. let mut active_token_positions = 0; for i in 0..old_header.token_count() { let src = old_header.token_offset(i); let pos: &TokenPosition = get_helper(dynamic, src); if !pos.is_active() { continue; } if i != active_token_positions { let dst = old_header.token_offset(active_token_positions); unsafe { sol_memmove( &mut dynamic[dst], &mut dynamic[src], size_of::(), ); } } active_token_positions += 1; } let mut active_serum3_orders = 0; for i in 0..old_header.serum3_count() { let src = old_header.serum3_offset(i); let pos: &Serum3Orders = get_helper(dynamic, src); if !pos.is_active() { continue; } if i != active_serum3_orders { let dst = old_header.serum3_offset(active_serum3_orders); unsafe { sol_memmove( &mut dynamic[dst], &mut dynamic[src], size_of::(), ); } } active_serum3_orders += 1; } let mut active_perp_positions = 0; for i in 0..old_header.perp_count() { let src = old_header.perp_offset(i); let pos: &PerpPosition = get_helper(dynamic, src); if !pos.is_active() { continue; } if i != active_perp_positions { let dst = old_header.perp_offset(active_perp_positions); unsafe { sol_memmove( &mut dynamic[dst], &mut dynamic[src], size_of::(), ); } } active_perp_positions += 1; } // Can't rearrange perp oo because LeafNodes store indexes, so the equivalent // to the "active" count for the other blocks is the max active index + 1. let mut blocked_perp_oo = 0; for i in 0..old_header.perp_oo_count() { let idx = old_header.perp_oo_count() - 1 - i; let src = old_header.perp_oo_offset(idx); let pos: &PerpOpenOrder = get_helper(dynamic, src); if pos.is_active() { blocked_perp_oo = idx + 1; break; } } let mut active_tcs = 0; for i in 0..old_header.token_conditional_swap_count() { let src = old_header.token_conditional_swap_offset(i); let pos: &TokenConditionalSwap = get_helper(dynamic, src); if !pos.is_configured() { continue; } if i != active_tcs { let dst = old_header.token_conditional_swap_offset(active_tcs); unsafe { sol_memmove( &mut dynamic[dst], &mut dynamic[src], size_of::(), ); } } active_tcs += 1; } // Check that the new allocations can fit the existing data require_gte!(new_header.token_count(), active_token_positions); require_gte!(new_header.serum3_count(), active_serum3_orders); require_gte!(new_header.perp_count(), active_perp_positions); require_gte!(new_header.perp_oo_count(), blocked_perp_oo); require_gte!(new_header.token_conditional_swap_count(), active_tcs); // First move pass: go left-to-right and move any blocks that need to be moved // to the left. This will never overwrite other data, because: // - moving to the left can only overwrite data to the left // - the left of the target location is >= the right of the previous data location // because either the previous was already moved to the left (clearly good), // or still needs to be moved to the right (the new end will be <= the target start) { // Token positions never move let old_serum3_start = old_header.serum3_offset(0); let new_serum3_start = new_header.serum3_offset(0); if new_serum3_start < old_serum3_start && active_serum3_orders > 0 { unsafe { sol_memmove( &mut dynamic[new_serum3_start], &mut dynamic[old_serum3_start], size_of::() * active_serum3_orders, ); } } let old_perp_start = old_header.perp_offset(0); let new_perp_start = new_header.perp_offset(0); if new_perp_start < old_perp_start && active_perp_positions > 0 { unsafe { sol_memmove( &mut dynamic[new_perp_start], &mut dynamic[old_perp_start], size_of::() * active_perp_positions, ); } } let old_perp_oo_start = old_header.perp_oo_offset(0); let new_perp_oo_start = new_header.perp_oo_offset(0); if new_perp_oo_start < old_perp_oo_start && blocked_perp_oo > 0 { unsafe { sol_memmove( &mut dynamic[new_perp_oo_start], &mut dynamic[old_perp_oo_start], size_of::() * blocked_perp_oo, ); } } let old_tcs_start = old_header.token_conditional_swap_offset(0); let new_tcs_start = new_header.token_conditional_swap_offset(0); if new_tcs_start < old_tcs_start && active_tcs > 0 { unsafe { sol_memmove( &mut dynamic[new_tcs_start], &mut dynamic[old_tcs_start], size_of::() * active_tcs, ); } } } // Second move pass: Go right-to-left and move everything to the right if needed. // This will never overwrite other data: // - because of moving right, it could only overwrite a block to the right // - if the block to the right needed moving to the right, that was already done // - if the block to the right was moved to the left, we know that its start will // be >= our block's end { let old_tcs_start = old_header.token_conditional_swap_offset(0); let new_tcs_start = new_header.token_conditional_swap_offset(0); if new_tcs_start > old_tcs_start && active_tcs > 0 { unsafe { sol_memmove( &mut dynamic[new_tcs_start], &mut dynamic[old_tcs_start], size_of::() * active_tcs, ); } } let old_perp_oo_start = old_header.perp_oo_offset(0); let new_perp_oo_start = new_header.perp_oo_offset(0); if new_perp_oo_start > old_perp_oo_start && blocked_perp_oo > 0 { unsafe { sol_memmove( &mut dynamic[new_perp_oo_start], &mut dynamic[old_perp_oo_start], size_of::() * blocked_perp_oo, ); } } let old_perp_start = old_header.perp_offset(0); let new_perp_start = new_header.perp_offset(0); if new_perp_start > old_perp_start && active_perp_positions > 0 { unsafe { sol_memmove( &mut dynamic[new_perp_start], &mut dynamic[old_perp_start], size_of::() * active_perp_positions, ); } } let old_serum3_start = old_header.serum3_offset(0); let new_serum3_start = new_header.serum3_offset(0); if new_serum3_start > old_serum3_start && active_serum3_orders > 0 { unsafe { sol_memmove( &mut dynamic[new_serum3_start], &mut dynamic[old_serum3_start], size_of::() * active_serum3_orders, ); } } // Token positions never move } // Defaulting pass: The blocks are in their final positions, clear out all unused slots { for i in active_token_positions..new_header.token_count() { *get_helper_mut(dynamic, new_header.token_offset(i)) = TokenPosition::default(); } for i in active_serum3_orders..new_header.serum3_count() { *get_helper_mut(dynamic, new_header.serum3_offset(i)) = Serum3Orders::default(); } for i in active_perp_positions..new_header.perp_count() { *get_helper_mut(dynamic, new_header.perp_offset(i)) = PerpPosition::default(); } for i in blocked_perp_oo..new_header.perp_oo_count() { *get_helper_mut(dynamic, new_header.perp_oo_offset(i)) = PerpOpenOrder::default(); } for i in active_tcs..new_header.token_conditional_swap_count() { *get_helper_mut(dynamic, new_header.token_conditional_swap_offset(i)) = TokenConditionalSwap::default(); } } { let offset = new_header.reserved_bytes_offset(); dynamic[offset..offset + DYNAMIC_RESERVED_BYTES] .copy_from_slice(&[0u8; DYNAMIC_RESERVED_BYTES]); } // update the already-parsed header *self.header_mut() = new_header; // write new lengths to the dynamic data (uses header) self.write_token_length(); self.write_serum3_length(); self.write_perp_length(); self.write_perp_oo_length(); self.write_token_conditional_swap_length(); Ok(()) } } /// 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 bytemuck::Zeroable; use itertools::Itertools; use std::path::PathBuf; use crate::state::PostOrderType; use super::*; fn make_test_account() -> MangoAccountValue { let account = MangoAccount::default_for_tests(); let bytes = AnchorSerialize::try_to_vec(&account).unwrap(); // Verify that the size is as expected let expected_space = MangoAccount::space( account.tokens.len() as u8, account.serum3.len() as u8, account.perps.len() as u8, account.perp_open_orders.len() as u8, account.token_conditional_swaps.len() as u8, ); assert_eq!(expected_space, 8 + bytes.len()); MangoAccountValue::from_bytes(&bytes).unwrap() } #[test] fn test_serialization_match() { let mut account = MangoAccount::default_for_tests(); account.group = Pubkey::new_unique(); account.owner = Pubkey::new_unique(); account.name = crate::util::fill_from_str("abcdef").unwrap(); account.delegate = Pubkey::new_unique(); account.account_num = 1; account.being_liquidated = 2; account.in_health_region = 3; account.bump = 4; account.net_deposits = 5; account.health_region_begin_init_health = 7; account.buyback_fees_accrued_current = 10; account.buyback_fees_accrued_previous = 11; account.buyback_fees_expiry_timestamp = 12; account.tokens.resize(8, TokenPosition::default()); account.tokens[0].token_index = 8; account.serum3.resize(8, Serum3Orders::default()); account.perps.resize(4, PerpPosition::default()); account.perps[0].market_index = 9; account.perp_open_orders.resize(8, PerpOpenOrder::default()); account.next_token_conditional_swap_id = 13; account .token_conditional_swaps .resize(12, TokenConditionalSwap::default()); account.token_conditional_swaps[0].buy_token_index = 14; let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap(); assert_eq!(8 + account_bytes.len(), MangoAccount::space(8, 8, 4, 8, 12)); let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap(); assert_eq!(account.group, account2.fixed.group); assert_eq!(account.owner, account2.fixed.owner); assert_eq!(account.name, account2.fixed.name); assert_eq!(account.delegate, account2.fixed.delegate); assert_eq!(account.account_num, account2.fixed.account_num); assert_eq!(account.being_liquidated, account2.fixed.being_liquidated); assert_eq!(account.in_health_region, account2.fixed.in_health_region); assert_eq!(account.bump, account2.fixed.bump); assert_eq!(account.net_deposits, account2.fixed.net_deposits); assert_eq!( account.perp_spot_transfers, account2.fixed.perp_spot_transfers ); assert_eq!( account.health_region_begin_init_health, account2.fixed.health_region_begin_init_health ); assert_eq!( account.buyback_fees_accrued_current, account2.fixed.buyback_fees_accrued_current ); assert_eq!( account.buyback_fees_accrued_previous, account2.fixed.buyback_fees_accrued_previous ); assert_eq!( account.buyback_fees_expiry_timestamp, account2.fixed.buyback_fees_expiry_timestamp ); assert_eq!( account.next_token_conditional_swap_id, account2.fixed.next_token_conditional_swap_id ); assert_eq!( account.tokens[0].token_index, account2 .token_position_by_raw_index_unchecked(0) .token_index ); assert_eq!( account.serum3[0].open_orders, account2.serum3_orders_by_raw_index_unchecked(0).open_orders ); assert_eq!( account.perps[0].market_index, account2 .perp_position_by_raw_index_unchecked(0) .market_index ); assert_eq!( account.token_conditional_swaps.len(), account2.all_token_conditional_swaps().count() ); assert_eq!( account.token_conditional_swaps[0].buy_token_index, account2 .token_conditional_swap_by_index(0) .unwrap() .buy_token_index ); } #[test] fn test_token_positions() { let mut account = make_test_account(); assert!(account.token_position(1).is_err()); assert!(account.token_position_and_raw_index(2).is_err()); assert!(account.token_position_mut(3).is_err()); assert_eq!( account.token_position_by_raw_index_unchecked(0).token_index, TokenIndex::MAX ); { let (pos, raw, active) = account.ensure_token_position(1).unwrap(); assert_eq!(raw, 0); assert_eq!(active, 0); assert_eq!(pos.token_index, 1); } { let (pos, raw, active) = account.ensure_token_position(7).unwrap(); assert_eq!(raw, 1); assert_eq!(active, 1); assert_eq!(pos.token_index, 7); } { let (pos, raw, active) = account.ensure_token_position(42).unwrap(); assert_eq!(raw, 2); assert_eq!(active, 2); assert_eq!(pos.token_index, 42); } { account.deactivate_token_position(1); let (pos, raw, active) = account.ensure_token_position(42).unwrap(); assert_eq!(raw, 2); assert_eq!(active, 1); assert_eq!(pos.token_index, 42); let (pos, raw, active) = account.ensure_token_position(8).unwrap(); assert_eq!(raw, 1); assert_eq!(active, 1); assert_eq!(pos.token_index, 8); } assert_eq!(account.active_token_positions().count(), 3); account.deactivate_token_position(0); assert_eq!( account.token_position_by_raw_index_unchecked(0).token_index, TokenIndex::MAX ); assert!(account.token_position(1).is_err()); assert!(account.token_position_mut(1).is_err()); assert!(account.token_position(8).is_ok()); assert!(account.token_position(42).is_ok()); assert_eq!(account.token_position_and_raw_index(42).unwrap().1, 2); assert_eq!(account.active_token_positions().count(), 2); { let (pos, raw) = account.token_position_mut(42).unwrap(); assert_eq!(pos.token_index, 42); assert_eq!(raw, 2); } { let (pos, raw) = account.token_position_mut(8).unwrap(); assert_eq!(pos.token_index, 8); assert_eq!(raw, 1); } } #[test] fn test_serum3_orders() { let mut account = make_test_account(); assert!(account.serum3_orders(1).is_err()); assert!(account.serum3_orders_mut(3).is_err()); assert_eq!( account.serum3_orders_by_raw_index_unchecked(0).market_index, Serum3MarketIndex::MAX ); assert_eq!(account.create_serum3_orders(1).unwrap().market_index, 1); assert_eq!(account.create_serum3_orders(7).unwrap().market_index, 7); assert_eq!(account.create_serum3_orders(42).unwrap().market_index, 42); assert!(account.create_serum3_orders(7).is_err()); assert_eq!(account.active_serum3_orders().count(), 3); assert!(account.deactivate_serum3_orders(7).is_ok()); assert_eq!( account.serum3_orders_by_raw_index_unchecked(1).market_index, Serum3MarketIndex::MAX ); assert!(account.create_serum3_orders(8).is_ok()); assert_eq!( account.serum3_orders_by_raw_index_unchecked(1).market_index, 8 ); assert_eq!(account.active_serum3_orders().count(), 3); assert!(account.deactivate_serum3_orders(1).is_ok()); assert!(account.serum3_orders(1).is_err()); assert!(account.serum3_orders_mut(1).is_err()); assert!(account.serum3_orders(8).is_ok()); assert!(account.serum3_orders(42).is_ok()); assert_eq!(account.active_serum3_orders().count(), 2); assert_eq!(account.serum3_orders_mut(42).unwrap().market_index, 42); assert_eq!(account.serum3_orders_mut(8).unwrap().market_index, 8); assert!(account.serum3_orders_mut(7).is_err()); } #[test] fn test_perp_positions() { let mut account = make_test_account(); assert!(account.perp_position(1).is_err()); assert!(account.perp_position_mut(3).is_err()); assert_eq!( account.perp_position_by_raw_index_unchecked(0).market_index, PerpMarketIndex::MAX ); { let (pos, raw) = account.ensure_perp_position(1, 0).unwrap(); assert_eq!(raw, 0); assert_eq!(pos.market_index, 1); assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 1); } { let (pos, raw) = account.ensure_perp_position(7, 0).unwrap(); assert_eq!(raw, 1); assert_eq!(pos.market_index, 7); assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 2); } { let (pos, raw) = account.ensure_perp_position(42, 0).unwrap(); assert_eq!(raw, 2); assert_eq!(pos.market_index, 42); assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 3); } { let pos_res = account.perp_position_mut(1); assert!(pos_res.is_ok()); assert_eq!(pos_res.unwrap().market_index, 1) } { let pos_res = account.perp_position_mut(99); assert!(pos_res.is_err()); } { assert!(account.deactivate_perp_position(7, 0).is_ok()); let (pos, raw) = account.ensure_perp_position(42, 0).unwrap(); assert_eq!(raw, 2); assert_eq!(pos.market_index, 42); assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 2); let (pos, raw) = account.ensure_perp_position(8, 0).unwrap(); assert_eq!(raw, 1); assert_eq!(pos.market_index, 8); assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 3); } assert_eq!(account.active_perp_positions().count(), 3); assert!(account.deactivate_perp_position(1, 0).is_ok()); assert_eq!( account.perp_position_by_raw_index_unchecked(0).market_index, PerpMarketIndex::MAX ); assert!(account.perp_position(1).is_err()); assert!(account.perp_position_mut(1).is_err()); assert!(account.perp_position(8).is_ok()); assert!(account.perp_position(42).is_ok()); assert_eq!(account.active_perp_positions().count(), 2); } #[test] fn test_buyback_fees() { let mut account = make_test_account(); let fixed = account.fixed_mut(); assert_eq!(fixed.buyback_fees_accrued(), 0); fixed.expire_buyback_fees(1000, 10); assert_eq!(fixed.buyback_fees_accrued(), 0); assert_eq!(fixed.buyback_fees_expiry_timestamp, 1010); fixed.accrue_buyback_fees(10); fixed.accrue_buyback_fees(5); assert_eq!(fixed.buyback_fees_accrued(), 15); fixed.reduce_buyback_fees_accrued(2); assert_eq!(fixed.buyback_fees_accrued(), 13); fixed.expire_buyback_fees(1009, 10); assert_eq!(fixed.buyback_fees_expiry_timestamp, 1010); assert_eq!(fixed.buyback_fees_accrued(), 13); assert_eq!(fixed.buyback_fees_accrued_current, 13); fixed.expire_buyback_fees(1010, 10); assert_eq!(fixed.buyback_fees_expiry_timestamp, 1020); assert_eq!(fixed.buyback_fees_accrued(), 13); assert_eq!(fixed.buyback_fees_accrued_previous, 13); assert_eq!(fixed.buyback_fees_accrued_current, 0); fixed.accrue_buyback_fees(5); assert_eq!(fixed.buyback_fees_accrued(), 18); fixed.reduce_buyback_fees_accrued(15); assert_eq!(fixed.buyback_fees_accrued(), 3); assert_eq!(fixed.buyback_fees_accrued_previous, 0); assert_eq!(fixed.buyback_fees_accrued_current, 3); fixed.expire_buyback_fees(1021, 10); fixed.accrue_buyback_fees(1); assert_eq!(fixed.buyback_fees_expiry_timestamp, 1030); assert_eq!(fixed.buyback_fees_accrued_previous, 3); assert_eq!(fixed.buyback_fees_accrued_current, 1); fixed.expire_buyback_fees(1051, 10); assert_eq!(fixed.buyback_fees_expiry_timestamp, 1060); assert_eq!(fixed.buyback_fees_accrued_previous, 0); assert_eq!(fixed.buyback_fees_accrued_current, 0); fixed.accrue_buyback_fees(7); fixed.expire_buyback_fees(1060, 10); fixed.accrue_buyback_fees(5); assert_eq!(fixed.buyback_fees_expiry_timestamp, 1070); assert_eq!(fixed.buyback_fees_accrued(), 12); fixed.reduce_buyback_fees_accrued(12); assert_eq!(fixed.buyback_fees_accrued(), 0); } #[test] fn test_token_conditional_swap() { let mut account = make_test_account(); assert_eq!(account.all_token_conditional_swaps().count(), 2); assert_eq!(account.active_token_conditional_swaps().count(), 0); assert_eq!(account.token_conditional_swap_free_index().unwrap(), 0); let tcs = account.free_token_conditional_swap_mut().unwrap(); tcs.id = 123; tcs.is_configured = 1; assert_eq!(account.all_token_conditional_swaps().count(), 2); assert_eq!(account.active_token_conditional_swaps().count(), 1); assert_eq!(account.token_conditional_swap_free_index().unwrap(), 1); let tcs = account.free_token_conditional_swap_mut().unwrap(); tcs.id = 234; tcs.is_configured = 1; assert_eq!(account.all_token_conditional_swaps().count(), 2); assert_eq!(account.active_token_conditional_swaps().count(), 2); let (index, tcs) = account.token_conditional_swap_by_id(123).unwrap(); assert_eq!(index, 0); assert_eq!(tcs.id, 123); let tcs = account.token_conditional_swap_by_index(0).unwrap(); assert_eq!(tcs.id, 123); let (index, tcs) = account.token_conditional_swap_by_id(234).unwrap(); assert_eq!(index, 1); assert_eq!(tcs.id, 234); let tcs = account.token_conditional_swap_by_index(1).unwrap(); assert_eq!(tcs.id, 234); assert!(account.free_token_conditional_swap_mut().is_err()); assert!(account.token_conditional_swap_free_index().is_err()); let tcs = account.token_conditional_swap_mut_by_index(0).unwrap(); tcs.is_configured = 0; assert_eq!(account.all_token_conditional_swaps().count(), 2); assert_eq!(account.active_token_conditional_swaps().count(), 1); assert_eq!( account.active_token_conditional_swaps().next().unwrap().id, 234 ); assert!(account.token_conditional_swap_by_id(123).is_err()); assert_eq!(account.token_conditional_swap_free_index().unwrap(), 0); let tcs = account.free_token_conditional_swap_mut().unwrap(); assert_eq!(tcs.id, 123); // old data } fn make_resize_test_account(header: &MangoAccountDynamicHeader) -> MangoAccountValue { let mut account = MangoAccount::default_for_tests(); account .tokens .resize(header.token_count(), TokenPosition::default()); account .serum3 .resize(header.serum3_count(), Serum3Orders::default()); account .perps .resize(header.perp_count(), PerpPosition::default()); account .perp_open_orders .resize(header.perp_oo_count(), PerpOpenOrder::default()); let mut bytes = AnchorSerialize::try_to_vec(&account).unwrap(); // The MangoAccount struct is missing some dynamic fields, add space for them let expected_space = header.account_size(); bytes.extend(vec![0u8; expected_space - bytes.len()]); // Set the length of these dynamic parts let (fixed, dynamic) = bytes.split_at_mut(size_of::()); let mut out_header = MangoAccountDynamicHeader::from_bytes(dynamic).unwrap(); out_header.token_conditional_swap_count = header.token_conditional_swap_count; let mut account = MangoAccountRefMut { header: &mut out_header, fixed: bytemuck::from_bytes_mut(fixed), dynamic, }; account.write_token_conditional_swap_length(); MangoAccountValue::from_bytes(&bytes).unwrap() } fn check_account_active_and_order( account: &MangoAccountValue, active: &MangoAccountDynamicHeader, ) -> Result<()> { let header = account.header(); assert_eq!(account.all_token_positions().count(), header.token_count()); assert_eq!( account.active_token_positions().count(), active.token_count() ); for i in 0..active.token_count() { assert_eq!( account.token_position_by_raw_index(i)?.token_index, i as TokenIndex ); } for i in active.token_count()..header.token_count() { let def = TokenPosition::default().try_to_vec().unwrap(); assert_eq!( account .token_position_by_raw_index(i)? .try_to_vec() .unwrap(), def ); } assert_eq!(account.all_serum3_orders().count(), header.serum3_count()); assert_eq!( account.active_serum3_orders().count(), active.serum3_count() ); for i in 0..active.serum3_count() { assert_eq!( account.serum3_orders_by_raw_index(i)?.market_index, i as Serum3MarketIndex ); } for i in active.serum3_count()..header.serum3_count() { let def = Serum3Orders::default().try_to_vec().unwrap(); assert_eq!( account.serum3_orders_by_raw_index(i)?.try_to_vec().unwrap(), def ); } assert_eq!(account.all_perp_positions().count(), header.perp_count()); assert_eq!(account.active_perp_positions().count(), active.perp_count()); for i in 0..active.perp_count() { assert_eq!( account.perp_position_by_raw_index(i)?.market_index, i as PerpMarketIndex ); } for i in active.perp_count()..header.perp_count() { let def = PerpPosition::default().try_to_vec().unwrap(); assert_eq!( account.perp_position_by_raw_index(i)?.try_to_vec().unwrap(), def ); } for i in 0..header.perp_oo_count() { let perp_oo = account.perp_order_by_raw_index(i)?; if i + 1 == active.perp_oo_count() { assert_eq!(perp_oo.market, 0); } else { let def = PerpOpenOrder::default().try_to_vec().unwrap(); assert_eq!(perp_oo.try_to_vec().unwrap(), def); } } assert_eq!( account.all_token_conditional_swaps().count(), header.token_conditional_swap_count() ); assert_eq!( account.active_token_conditional_swaps().count(), active.token_conditional_swap_count() ); for i in 0..active.token_conditional_swap_count() { assert_eq!(account.token_conditional_swap_by_index(i)?.id, i as u64); } for i in active.token_conditional_swap_count()..header.token_conditional_swap_count() { let def = TokenConditionalSwap::default().try_to_vec().unwrap(); assert_eq!( account .token_conditional_swap_by_index(i)? .try_to_vec() .unwrap(), def ); } assert!(account.dynamic_reserved_bytes().iter().all(|&v| v == 0)); Ok(()) } #[test] fn test_account_resize_fixed() -> Result<()> { let header = MangoAccountDynamicHeader { token_count: 4, serum3_count: 5, perp_count: 6, perp_oo_count: 7, token_conditional_swap_count: 8, }; let mut account = make_resize_test_account(&header); // setup positions and leave gaps account.ensure_token_position(7)?; account.ensure_token_position(0)?; account.ensure_token_position(8)?; account.ensure_token_position(1)?; account.deactivate_token_position(0); account.deactivate_token_position(2); account.create_serum3_orders(0)?; account.create_serum3_orders(7)?; account.create_serum3_orders(1)?; *account.serum3_orders_mut_by_raw_index(1) = Serum3Orders::default(); account.ensure_perp_position(0, 0)?; account.ensure_perp_position(1, 0)?; account.ensure_perp_position(2, 0)?; account.ensure_perp_position(7, 0)?; account.ensure_perp_position(8, 0)?; account.ensure_perp_position(3, 0)?; account.deactivate_perp_position(7, 0)?; account.deactivate_perp_position(8, 0)?; let mut perp_oo = account.perp_order_mut_by_raw_index(4); perp_oo.market = 0; let mut make_tcs = |raw_index: usize, id| { let mut tcs = account .token_conditional_swap_mut_by_index(raw_index) .unwrap(); tcs.set_is_configured(true); tcs.id = id; }; make_tcs(2, 0); make_tcs(4, 1); let active = MangoAccountDynamicHeader { token_count: 2, serum3_count: 2, perp_count: 4, perp_oo_count: 5, token_conditional_swap_count: 2, }; // Resizing to the same size just removes the empty spaces { let mut ta = account.clone(); ta.resize_dynamic_content( header.token_count, header.serum3_count, header.perp_count, header.perp_oo_count, header.token_conditional_swap_count, )?; check_account_active_and_order(&ta, &active)?; } // Resizing to the minimum size is fine { let mut ta = account.clone(); ta.resize_dynamic_content( active.token_count, active.serum3_count, active.perp_count, active.perp_oo_count, active.token_conditional_swap_count, )?; check_account_active_and_order(&ta, &active)?; } // Resizing to less than what is active is forbidden { let mut ta = account.clone(); ta.resize_dynamic_content( active.token_count - 1, active.serum3_count, active.perp_count, active.perp_oo_count, active.token_conditional_swap_count, ) .unwrap_err(); ta.resize_dynamic_content( active.token_count, active.serum3_count - 1, active.perp_count, active.perp_oo_count, active.token_conditional_swap_count, ) .unwrap_err(); ta.resize_dynamic_content( active.token_count, active.serum3_count, active.perp_count - 1, active.perp_oo_count, active.token_conditional_swap_count, ) .unwrap_err(); ta.resize_dynamic_content( active.token_count, active.serum3_count, active.perp_count, active.perp_oo_count - 1, active.token_conditional_swap_count, ) .unwrap_err(); ta.resize_dynamic_content( active.token_count, active.serum3_count, active.perp_count, active.perp_oo_count, active.token_conditional_swap_count - 1, ) .unwrap_err(); } Ok(()) } #[test] fn test_account_resize_random() -> Result<()> { use rand::{seq::SliceRandom, Rng}; let mut rng = rand::thread_rng(); for _ in 0..1000 { let header = MangoAccountDynamicHeader { token_count: 4, serum3_count: 4, perp_count: 4, perp_oo_count: 8, token_conditional_swap_count: 4, }; let mut account = make_resize_test_account(&header); let active = MangoAccountDynamicHeader { token_count: rng.gen_range(0..header.token_count + 1), serum3_count: rng.gen_range(0..header.serum3_count + 1), perp_count: rng.gen_range(0..header.perp_count + 1), perp_oo_count: rng.gen_range(0..header.perp_oo_count + 1), token_conditional_swap_count: rng .gen_range(0..header.token_conditional_swap_count + 1), }; let options = (0..header.token_count()).collect_vec(); let selected = options.choose_multiple(&mut rng, active.token_count()); for (i, index) in selected.sorted().enumerate() { account.token_position_mut_by_raw_index(*index).token_index = i as TokenIndex; } let options = (0..header.serum3_count()).collect_vec(); let selected = options.choose_multiple(&mut rng, active.serum3_count()); for (i, index) in selected.sorted().enumerate() { account.serum3_orders_mut_by_raw_index(*index).market_index = i as Serum3MarketIndex; } let options = (0..header.perp_count()).collect_vec(); let selected = options.choose_multiple(&mut rng, active.perp_count()); for (i, index) in selected.sorted().enumerate() { account.perp_position_mut_by_raw_index(*index).market_index = i as PerpMarketIndex; } if active.perp_oo_count() > 0 { let mut perp_oo = account.perp_order_mut_by_raw_index(active.perp_oo_count() - 1); perp_oo.market = 0; } let options = (0..header.token_conditional_swap_count()).collect_vec(); let selected = options.choose_multiple(&mut rng, active.token_conditional_swap_count()); for (i, index) in selected.sorted().enumerate() { let tcs = account.token_conditional_swap_mut_by_index(*index).unwrap(); tcs.set_is_configured(true); tcs.id = i as u64; } let target = MangoAccountDynamicHeader { token_count: rng.gen_range(active.token_count..6), serum3_count: rng.gen_range(active.serum3_count..7), perp_count: rng.gen_range(active.perp_count..6), perp_oo_count: rng.gen_range(active.perp_oo_count..16), token_conditional_swap_count: rng.gen_range(active.token_conditional_swap_count..8), }; let target_size = target.account_size(); if target_size > account.dynamic.len() { account .dynamic .extend(vec![0u8; target_size - account.dynamic.len()]); } account .resize_dynamic_content( target.token_count, target.serum3_count, target.perp_count, target.perp_oo_count, target.token_conditional_swap_count, ) .unwrap(); check_account_active_and_order(&account, &active).unwrap(); } Ok(()) } #[test] fn test_perp_order_events() -> Result<()> { let group = Group::zeroed(); let perp_market_index = 0; let mut perp_market = PerpMarket::zeroed(); let mut account = make_test_account(); account.ensure_token_position(0)?; account.ensure_perp_position(perp_market_index, 0)?; let owner = Pubkey::new_unique(); let slot = account.perp_next_order_slot()?; let order_id = 127; let quantity = 42; let order = LeafNode::new( slot as u8, order_id, owner, quantity, 1, PostOrderType::Limit, 0, 0, 0, ); let side = Side::Bid; account.add_perp_order(0, side, BookSideOrderTree::Fixed, &order)?; let make_fill = |quantity, out, order_id| { FillEvent::new( side.invert_side(), out, slot as u8, 0, 0, owner, order_id, 0, I80F48::ZERO, 0, owner, 0, I80F48::ZERO, 1, quantity, ) }; let pp = |a: &MangoAccountValue| a.perp_position(perp_market_index).unwrap().clone(); { // full fill let mut account = account.clone(); let fill = make_fill(quantity, true, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); } { // full fill, no order id let mut account = account.clone(); let fill = make_fill(quantity, true, 0); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); } { // out event let mut account = account.clone(); account.execute_perp_out_event(perp_market_index, side, slot, quantity, order_id)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); } { // out event, no order id let mut account = account.clone(); account.execute_perp_out_event(perp_market_index, side, slot, quantity, 0)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); } { // cancel let mut account = account.clone(); account.remove_perp_order(slot, quantity)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); } { // partial fill event, user closes rest, following out event has no effect let mut account = account.clone(); let fill = make_fill(quantity - 10, false, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, 10); assert_eq!(pp(&account).asks_base_lots, 0); assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, 10); // out event happens but is delayed account.remove_perp_order(slot, 0)?; assert_eq!(pp(&account).bids_base_lots, 10); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); account.execute_perp_out_event(perp_market_index, side, slot, 10, order_id)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); } { // partial fill and out are delayed, user closes first let mut account = account.clone(); account.remove_perp_order(slot, 0)?; assert_eq!(pp(&account).bids_base_lots, quantity); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); let fill = make_fill(quantity - 10, false, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, 10); assert_eq!(pp(&account).asks_base_lots, 0); account.execute_perp_out_event(perp_market_index, side, slot, 10, order_id)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); } { // partial fill and cancel, cancel before outevent let mut account = account.clone(); account.remove_perp_order(slot, 10)?; assert_eq!(pp(&account).bids_base_lots, quantity - 10); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); let fill = make_fill(quantity - 10, false, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); } { // several fills let mut account = account.clone(); let fill = make_fill(10, false, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, quantity - 10); assert_eq!(pp(&account).asks_base_lots, 0); assert_eq!( account.perp_order_by_raw_index(slot)?.quantity, quantity - 10 ); let fill = make_fill(10, false, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, quantity - 20); assert_eq!(pp(&account).asks_base_lots, 0); assert_eq!( account.perp_order_by_raw_index(slot)?.quantity, quantity - 20 ); let fill = make_fill(quantity - 20, true, order_id); account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, 0); assert_eq!(pp(&account).asks_base_lots, 0); assert!(!account.perp_order_by_raw_index(0)?.is_active()); } { // mismatched fill and out let mut account = account.clone(); let mut fill = make_fill(10, false, order_id); fill.maker_order_id = 1; account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?; assert_eq!(pp(&account).bids_base_lots, quantity - 10); assert_eq!(pp(&account).asks_base_lots, 0); assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, quantity); account.execute_perp_out_event(perp_market_index, side, slot, 10, 1)?; assert_eq!(pp(&account).bids_base_lots, quantity - 20); assert_eq!(pp(&account).asks_base_lots, 0); assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, quantity); } Ok(()) } #[test] fn test_perp_auto_close_first_unused() { let mut account = make_test_account(); // Fill all perp slots assert_eq!(account.header.perp_count, 4); account.ensure_perp_position(1, 0).unwrap(); account.ensure_perp_position(2, 0).unwrap(); account.ensure_perp_position(3, 0).unwrap(); account.ensure_perp_position(4, 0).unwrap(); assert_eq!(account.active_perp_positions().count(), 4); // Force usage of some perp slot (leaves 3 unused) account.perp_position_mut(1).unwrap().taker_base_lots = 10; account.perp_position_mut(2).unwrap().base_position_lots = 10; account.perp_position_mut(4).unwrap().quote_position_native = I80F48::from_num(10); assert!(account.perp_position(3).ok().is_some()); // Should not succeed anymore { let e = account.ensure_perp_position(5, 0); assert!(e.is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code())); } // Act let to_be_closed_account_opt = account.find_first_active_unused_perp_position(); assert_eq!(to_be_closed_account_opt.unwrap().market_index, 3) } // Attempts reading old mango account data with borsh and with zerocopy #[test] fn test_mango_account_backwards_compatibility() -> Result<()> { use solana_program_test::{find_file, read_file}; let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.push("resources/test"); // Grab live accounts with // solana account CZGf1qbYPaSoabuA1EmdN8W5UHvH5CeXcNZ7RTx65aVQ --output-file programs/mango-v4/resources/test/mangoaccount-v0.21.3.bin let fixtures = vec!["mangoaccount-v0.21.3"]; for fixture in fixtures { let filename = format!("resources/test/{}.bin", fixture); let account_bytes = read_file(find_file(&filename).unwrap()); // Read with borsh let mut account_bytes_slice: &[u8] = &account_bytes; let borsh_account = MangoAccount::try_deserialize(&mut account_bytes_slice)?; // Read with zerocopy let zerocopy_reader = MangoAccountValue::from_bytes(&account_bytes[8..])?; let fixed = &zerocopy_reader.fixed; let zerocopy_account = MangoAccount { group: fixed.group, owner: fixed.owner, name: fixed.name, delegate: fixed.delegate, account_num: fixed.account_num, being_liquidated: fixed.being_liquidated, in_health_region: fixed.in_health_region, bump: fixed.bump, padding: Default::default(), net_deposits: fixed.net_deposits, perp_spot_transfers: fixed.perp_spot_transfers, health_region_begin_init_health: fixed.health_region_begin_init_health, frozen_until: fixed.frozen_until, buyback_fees_accrued_current: fixed.buyback_fees_accrued_current, buyback_fees_accrued_previous: fixed.buyback_fees_accrued_previous, buyback_fees_expiry_timestamp: fixed.buyback_fees_expiry_timestamp, next_token_conditional_swap_id: fixed.next_token_conditional_swap_id, temporary_delegate: fixed.temporary_delegate, temporary_delegate_expiry: fixed.temporary_delegate_expiry, last_collateral_fee_charge: fixed.last_collateral_fee_charge, reserved: [0u8; 152], header_version: *zerocopy_reader.header_version(), padding3: Default::default(), padding4: Default::default(), tokens: zerocopy_reader.all_token_positions().cloned().collect_vec(), padding5: Default::default(), serum3: zerocopy_reader.all_serum3_orders().cloned().collect_vec(), padding6: Default::default(), perps: zerocopy_reader.all_perp_positions().cloned().collect_vec(), padding7: Default::default(), perp_open_orders: zerocopy_reader.all_perp_orders().cloned().collect_vec(), padding8: Default::default(), token_conditional_swaps: zerocopy_reader .all_token_conditional_swaps() .cloned() .collect_vec(), reserved_dynamic: zerocopy_reader.dynamic_reserved_bytes().try_into().unwrap(), }; // Both methods agree? assert_eq!(borsh_account, zerocopy_account); // Serializing and deserializing produces the same data? let mut borsh_bytes = Vec::new(); borsh_account.try_serialize(&mut borsh_bytes)?; let mut slice: &[u8] = &borsh_bytes; let roundtrip_account = MangoAccount::try_deserialize(&mut slice)?; assert_eq!(borsh_account, roundtrip_account); } Ok(()) } }