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; use solana_program::program_memory::sol_memmove; use static_assertions::const_assert_eq; use crate::error::*; use crate::health::{HealthCache, HealthType}; use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog}; use super::dynamic_account::*; use super::BookSideOrderTree; use super::FillEvent; use super::LeafNode; use super::PerpMarket; use super::PerpMarketIndex; use super::PerpOpenOrder; use super::Serum3MarketIndex; use super::TokenIndex; use super::FREE_ORDER_SLOT; 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; // 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, } // 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 #[account] 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, 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, 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, pub reserved: [u8; 208], // dynamic pub header_version: u8, pub padding3: [u8; 7], // note: padding is required for TokenPosition, etc. to be aligned pub padding4: u32, // Maps token_index -> deposit/borrow account for each token // that is active on this MangoAccount. pub tokens: Vec, pub padding5: u32, // Maps serum_market_index -> open orders for each serum market // that is active on this MangoAccount. pub serum3: Vec, pub padding6: u32, pub perps: Vec, pub padding7: u32, pub perp_open_orders: Vec, } 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, health_region_begin_init_health: 0, frozen_until: 0, buyback_fees_accrued_current: 0, buyback_fees_accrued_previous: 0, buyback_fees_expiry_timestamp: 0, reserved: [0; 208], 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], perp_spot_transfers: 0, } } /// 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, ) -> Result { require_gte!(16, token_count); require_gte!(8, serum3_count); require_gte!(8, perp_count); require_gte!(64, perp_oo_count); Ok(8 + size_of::() + Self::dynamic_size(token_count, serum3_count, perp_count, perp_oo_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_size( 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)) } } // Mango Account fixed part for easy zero copy deserialization #[zero_copy] #[derive(bytemuck::Pod, bytemuck::Zeroable)] 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 reserved: [u8; 208], } const_assert_eq!(size_of::(), 32 * 4 + 8 + 7 * 8 + 208); 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.delegate == ix_signer } pub fn is_delegate(&self, ix_signer: Pubkey) -> bool { self.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. 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. pub fn reduce_buyback_fees_accrued(&mut self, amount: u64) { if amount > self.buyback_fees_accrued_previous { self.buyback_fees_accrued_current = self .buyback_fees_accrued_current .saturating_sub(amount - self.buyback_fees_accrued_previous); self.buyback_fees_accrued_previous = 0; } else { self.buyback_fees_accrued_previous -= amount; } } } 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)] pub struct MangoAccountDynamicHeader { pub token_count: u8, pub serum3_count: u8, pub perp_count: u8, pub perp_oo_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(); Ok(Self { token_count, serum3_count, perp_count, perp_oo_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 { // 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 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::() } 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() } } /// 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() } /// 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 fn token_position_by_raw_index(&self, raw_index: usize) -> &TokenPosition { get_helper(self.dynamic(), self.header().token_offset(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(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 fn serum3_orders_by_raw_index(&self, raw_index: usize) -> &Serum3Orders { get_helper(self.dynamic(), self.header().serum3_offset(raw_index)) } pub fn all_serum3_orders(&self) -> impl Iterator + '_ { (0..self.header().serum3_count()).map(|i| self.serum3_orders_by_raw_index(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 fn perp_position_by_raw_index(&self, raw_index: usize) -> &PerpPosition { get_helper(self.dynamic(), self.header().perp_offset(raw_index)) } pub fn all_perp_positions(&self) -> impl Iterator { (0..self.header().perp_count()).map(|i| self.perp_position_by_raw_index(i)) } pub fn active_perp_positions(&self) -> impl Iterator { self.all_perp_positions().filter(|p| p.is_active()) } pub fn perp_order_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder { get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index)) } pub fn all_perp_orders(&self) -> impl Iterator { (0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index(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<&PerpOpenOrder> { self.all_perp_orders() .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<&PerpOpenOrder> { self.all_perp_orders() .find(|&oo| oo.is_active_for_market(market_index) && oo.id == order_id) } pub fn being_liquidated(&self) -> bool { self.fixed().being_liquidated() } 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.deref_or_borrow().group; let token_position = self.token_position_mut_by_raw_index(raw_index); assert!(token_position.in_use_count == 0); emit!(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; } // 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 mut settle_token_position = self.ensure_token_position(settle_token_index)?.0; settle_token_position.in_use_count += 1; } } if let Some(raw_index) = raw_index_opt { Ok((self.perp_position_mut_by_raw_index(raw_index), raw_index)) } else { err!(MangoError::NoFreePerpPositionIndex) } } 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 mut settle_token_position = self.token_position_mut(settle_token_index)?.0; settle_token_position.in_use_count -= 1; 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.deref_or_borrow().group; let perp_position = self.perp_position_mut(perp_market_index)?; emit!(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 mut settle_token_position = self.token_position_mut(settle_token_index)?.0; settle_token_position.in_use_count -= 1; Ok(()) } pub fn add_perp_order( &mut self, perp_market_index: PerpMarketIndex, side: Side, order_tree: BookSideOrderTree, order: &LeafNode, client_order_id: u64, ) -> Result<()> { let mut perp_account = self.perp_position_mut(perp_market_index)?; match side { Side::Bid => { perp_account.bids_base_lots += order.quantity; } Side::Ask => { perp_account.asks_base_lots += order.quantity; } }; let slot = order.owner_slot as usize; let mut 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 = client_order_id; Ok(()) } pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> { { let oo = self.perp_order_mut_by_raw_index(slot); require_neq!(oo.market, FREE_ORDER_SLOT); let order_side = oo.side_and_tree().side(); let perp_market_index = oo.market; let perp_account = self.perp_position_mut(perp_market_index)?; // accounting match order_side { Side::Bid => { perp_account.bids_base_lots -= quantity; } Side::Ask => { perp_account.asks_base_lots -= quantity; } } } // release space let oo = self.perp_order_mut_by_raw_index(slot); oo.market = FREE_ORDER_SLOT; oo.side_and_tree = SideAndOrderTree::BidFixed.into(); oo.id = 0; oo.client_id = 0; Ok(()) } pub fn execute_perp_maker( &mut self, perp_market_index: PerpMarketIndex, perp_market: &mut PerpMarket, fill: &FillEvent, ) -> 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() { self.fixed_mut() .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); pa.record_trade(perp_market, base_change, quote); pa.maker_volume += quote.abs().to_num::(); if fill.maker_out() { self.remove_perp_order(fill.maker_slot as usize, base_change.abs()) } else { match side { Side::Bid => { pa.bids_base_lots -= base_change.abs(); } Side::Ask => { pa.asks_base_lots -= base_change.abs(); } } Ok(()) } } 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); pa.record_trade(perp_market, base_change, quote_change_native); pa.taker_volume += quote_change_native.abs().to_num::(); Ok(()) } 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); // 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(pre_init_health) } 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); require!( post_init_health >= 0 || post_init_health > pre_init_health, 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); } // 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 tokens_offset = self.header().token_offset(0); // msg!( // "writing tokens length at {}", // tokens_offset - size_of::() // ); let count = self.header().token_count; let dst: &mut [u8] = &mut self.dynamic_mut()[tokens_offset - BORSH_VEC_SIZE_BYTES..tokens_offset]; dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); } fn write_serum3_length(&mut self) { let serum3_offset = self.header().serum3_offset(0); // msg!( // "writing serum3 length at {}", // serum3_offset - size_of::() // ); let count = self.header().serum3_count; let dst: &mut [u8] = &mut self.dynamic_mut()[serum3_offset - BORSH_VEC_SIZE_BYTES..serum3_offset]; dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); } fn write_perp_length(&mut self) { let perp_offset = self.header().perp_offset(0); // msg!( // "writing perp length at {}", // perp_offset - size_of::() // ); let count = self.header().perp_count; let dst: &mut [u8] = &mut self.dynamic_mut()[perp_offset - BORSH_VEC_SIZE_BYTES..perp_offset]; dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); } fn write_perp_oo_length(&mut self) { let perp_oo_offset = self.header().perp_oo_offset(0); // msg!( // "writing perp length at {}", // perp_offset - size_of::() // ); let count = self.header().perp_oo_count; let dst: &mut [u8] = &mut self.dynamic_mut()[perp_oo_offset - BORSH_VEC_SIZE_BYTES..perp_oo_offset]; dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); } pub fn expand_dynamic_content( &mut self, new_token_count: u8, new_serum3_count: u8, new_perp_count: u8, new_perp_oo_count: u8, ) -> Result<()> { require_gte!(new_token_count, self.header().token_count); require_gte!(new_serum3_count, self.header().serum3_count); require_gte!(new_perp_count, self.header().perp_count); require_gte!(new_perp_oo_count, self.header().perp_oo_count); // create a temp copy to compute new starting offsets 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, }; let old_header = self.header().clone(); let dynamic = self.dynamic_mut(); // expand dynamic components by first moving existing positions, and then setting new ones to defaults // perp oo if old_header.perp_oo_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.perp_oo_offset(0)], &mut dynamic[old_header.perp_oo_offset(0)], size_of::() * old_header.perp_oo_count(), ); } } for i in old_header.perp_oo_count..new_perp_oo_count { *get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) = PerpOpenOrder::default(); } // perp positions if old_header.perp_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.perp_offset(0)], &mut dynamic[old_header.perp_offset(0)], size_of::() * old_header.perp_count(), ); } } for i in old_header.perp_count..new_perp_count { *get_helper_mut(dynamic, new_header.perp_offset(i.into())) = PerpPosition::default(); } // serum3 positions if old_header.serum3_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.serum3_offset(0)], &mut dynamic[old_header.serum3_offset(0)], size_of::() * old_header.serum3_count(), ); } } for i in old_header.serum3_count..new_serum3_count { *get_helper_mut(dynamic, new_header.serum3_offset(i.into())) = Serum3Orders::default(); } // token positions if old_header.token_count() > 0 { unsafe { sol_memmove( &mut dynamic[new_header.token_offset(0)], &mut dynamic[old_header.token_offset(0)], size_of::() * old_header.token_count(), ); } } for i in old_header.token_count..new_token_count { *get_helper_mut(dynamic, new_header.token_offset(i.into())) = TokenPosition::default(); } // 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(); 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 super::*; fn make_test_account() -> MangoAccountValue { let bytes = AnchorSerialize::try_to_vec(&MangoAccount::default_for_tests()).unwrap(); 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(8, PerpPosition::default()); account.perps[0].market_index = 9; account.perp_open_orders.resize(8, PerpOpenOrder::default()); let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap(); assert_eq!( 8 + account_bytes.len(), MangoAccount::space(8, 8, 8, 8).unwrap() ); 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.tokens[0].token_index, account2.token_position_by_raw_index(0).token_index ); assert_eq!( account.serum3[0].open_orders, account2.serum3_orders_by_raw_index(0).open_orders ); assert_eq!( account.perps[0].market_index, account2.perp_position_by_raw_index(0).market_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(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(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(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(1).market_index, Serum3MarketIndex::MAX ); assert!(account.create_serum3_orders(8).is_ok()); assert_eq!(account.serum3_orders_by_raw_index(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(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(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(100); assert_eq!(fixed.buyback_fees_accrued(), 0); } }