mango-v4/programs/mango-v4/src/state/mango_account.rs

2952 lines
110 KiB
Rust

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<TokenPosition>,
#[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<Serum3Orders>,
#[derivative(Debug = "ignore")]
pub padding6: u32,
pub perps: Vec<PerpPosition>,
#[derivative(Debug = "ignore")]
pub padding7: u32,
pub perp_open_orders: Vec<PerpOpenOrder>,
#[derivative(Debug = "ignore")]
pub padding8: u32,
pub token_conditional_swaps: Vec<TokenConditionalSwap>,
#[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::<MangoAccountFixed>()
+ 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::<TokenPosition>() * 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::<Serum3Orders>() * 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::<PerpPosition>() * 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::<PerpOpenOrder>() * 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::<TokenConditionalSwap>() * 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::<MangoAccountFixed>(),
32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152
);
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 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<Self> {
let header_version = u8::from_le_bytes(*array_ref![dynamic_data, 0, size_of::<u8>()]);
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<T: bytemuck::Pod>(data: &[u8], index: usize) -> &T {
bytemuck::from_bytes(&data[index..index + size_of::<T>()])
}
fn get_helper_mut<T: bytemuck::Pod>(data: &mut [u8], index: usize) -> &mut T {
bytemuck::from_bytes_mut(&mut data[index..index + size_of::<T>()])
}
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::<TokenPosition>()
}
// 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::<Serum3Orders>()
}
// 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::<PerpPosition>()
}
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::<PerpOpenOrder>()
}
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::<TokenConditionalSwap>()
}
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<MangoAccountDynamicHeader, MangoAccountFixed, Vec<u8>>;
/// 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<MangoAccountDynamicHeader, &'a MangoAccountFixed, &'a [u8]>;
/// Useful when loading from RefCell, like from AccountInfo
pub type MangoAccountLoadedRefCell<'a> =
DynamicAccount<MangoAccountDynamicHeader, Ref<'a, MangoAccountFixed>, Ref<'a, [u8]>>;
/// Useful when loading from RefCell, like from AccountInfo
pub type MangoAccountLoadedRefCellMut<'a> =
DynamicAccount<MangoAccountDynamicHeader, RefMut<'a, MangoAccountFixed>, RefMut<'a, [u8]>>;
impl MangoAccountValue {
// bytes without discriminator
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let (fixed, dynamic) = bytes.split_at(size_of::<MangoAccountFixed>());
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<Self> {
let (fixed, dynamic) = bytes.split_at(size_of::<MangoAccountFixed>());
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<MangoAccountDynamicHeader>,
Fixed: DerefOrBorrow<MangoAccountFixed>,
Dynamic: DerefOrBorrow<[u8]>,
> DynamicAccount<Header, Fixed, Dynamic>
{
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<Item = &TokenPosition> + '_ {
(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<Item = &TokenPosition> + '_ {
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<Item = &Serum3Orders> + '_ {
(0..self.header().serum3_count()).map(|i| self.serum3_orders_by_raw_index_unchecked(i))
}
pub fn active_serum3_orders(&self) -> impl Iterator<Item = &Serum3Orders> + '_ {
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<Item = &PerpPosition> {
(0..self.header().perp_count()).map(|i| self.perp_position_by_raw_index_unchecked(i))
}
pub fn active_perp_positions(&self) -> impl Iterator<Item = &PerpPosition> {
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<Item = &PerpOpenOrder> {
(0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index_unchecked(i))
}
pub fn perp_next_order_slot(&self) -> Result<usize> {
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<Item = &TokenConditionalSwap> {
(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<Item = &TokenConditionalSwap> {
self.all_token_conditional_swaps()
.filter(|p| p.is_configured())
}
pub fn token_conditional_swap_free_index(&self) -> Result<usize> {
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<MangoAccountDynamicHeader> + DerefOrBorrow<MangoAccountDynamicHeader>,
Fixed: DerefOrBorrowMut<MangoAccountFixed> + DerefOrBorrow<MangoAccountFixed>,
Dynamic: DerefOrBorrowMut<[u8]> + DerefOrBorrow<[u8]>,
> DynamicAccount<Header, Fixed, Dynamic>
{
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<I80F48> {
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::<u64>());
}
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::<u64>();
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<I80F48> {
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::<u64>();
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<I80F48> {
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<I80F48> {
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<CheckLiquidatable> {
// 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::<TokenPosition>(),
);
}
}
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::<Serum3Orders>(),
);
}
}
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::<PerpPosition>(),
);
}
}
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::<TokenConditionalSwap>(),
);
}
}
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::<Serum3Orders>() * 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::<PerpPosition>() * 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::<PerpOpenOrder>() * 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::<TokenConditionalSwap>() * 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::<TokenConditionalSwap>() * 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::<PerpOpenOrder>() * 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::<PerpPosition>() * 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::<Serum3Orders>() * 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<MangoAccountFixed> to create an accessor for the full account.
pub trait MangoAccountLoader<'a> {
fn load_full(self) -> Result<MangoAccountLoadedRefCell<'a>>;
fn load_full_mut(self) -> Result<MangoAccountLoadedRefCellMut<'a>>;
fn load_full_init(self) -> Result<MangoAccountLoadedRefCellMut<'a>>;
}
impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAccountFixed> {
fn load_full(self) -> Result<MangoAccountLoadedRefCell<'a>> {
// Error checking
self.load()?;
let data = self.as_ref().try_borrow_data()?;
let header =
MangoAccountDynamicHeader::from_bytes(&data[8 + size_of::<MangoAccountFixed>()..])?;
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::<MangoAccountFixed>()));
Ok(MangoAccountLoadedRefCell {
header,
fixed: Ref::map(fixed_bytes, |b| bytemuck::from_bytes(b)),
dynamic,
})
}
fn load_full_mut(self) -> Result<MangoAccountLoadedRefCellMut<'a>> {
// Error checking
self.load_mut()?;
let data = self.as_ref().try_borrow_mut_data()?;
let header =
MangoAccountDynamicHeader::from_bytes(&data[8 + size_of::<MangoAccountFixed>()..])?;
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::<MangoAccountFixed>()));
Ok(MangoAccountLoadedRefCellMut {
header,
fixed: RefMut::map(fixed_bytes, |b| bytemuck::from_bytes_mut(b)),
dynamic,
})
}
fn load_full_init(self) -> Result<MangoAccountLoadedRefCellMut<'a>> {
// 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::<MangoAccountFixed>()..])?;
}
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::<MangoAccountFixed>());
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(())
}
}