diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 3d8c524ff..bc83afaf9 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -1,11 +1,12 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; -use pyth_client::load_price; +use std::cell::Ref; use crate::error::MangoError; -use crate::state::{determine_oracle_type, Bank, MangoAccount, OracleType, StubOracle}; +use crate::state::{oracle_price, Bank, MangoAccount, TokenIndex}; use crate::util; use crate::util::checked_math as cm; +use crate::util::LoadZeroCopy; pub fn compute_health(account: &MangoAccount, ais: &[AccountInfo]) -> Result { let active_token_len = account.token_account_map.iter_active().count(); @@ -20,23 +21,44 @@ pub fn compute_health(account: &MangoAccount, ais: &[AccountInfo]) -> Result { + bank: Ref<'a, Bank>, + price: I80F48, +} + +fn find_price(token_index: TokenIndex, banks_and_prices: &[BankAndPrice]) -> Result { + Ok(banks_and_prices + .iter() + .find(|b| b.bank.token_index == token_index) + .ok_or(error!(MangoError::SomeError))? + .price) +} + fn compute_health_detail( account: &MangoAccount, banks: &[AccountInfo], oracles: &[AccountInfo], - _serum_oos: &[AccountInfo], + serum_oos: &[AccountInfo], ) -> Result { let mut assets = I80F48::ZERO; let mut liabilities = I80F48::ZERO; // absolute value - for (position, (bank_ai, oracle_ai)) in util::zip!( - account.token_account_map.iter_active(), - banks.iter(), - oracles.iter() - ) { - let bank_loader = AccountLoader::<'_, Bank>::try_from(bank_ai)?; - let bank = bank_loader.load()?; - // TODO: This assumes banks are passed in order - is that an ok assumption? + // collect the bank and oracle data once + let banks_and_prices = util::zip!(banks.iter(), oracles.iter()) + .map(|(bank_ai, oracle_ai)| { + let bank = bank_ai.load::()?; + require!(bank.oracle == oracle_ai.key(), MangoError::UnexpectedOracle); + let price = oracle_price(oracle_ai)?; + Ok(BankAndPrice { bank, price }) + }) + .collect::>>()?; + + // health contribution from token accounts + for (position, BankAndPrice { bank, price }) in util::zip!( + account.token_account_map.iter_active(), + banks_and_prices.iter() + ) { + // This assumes banks are passed in order require!( bank.token_index == position.token_index, MangoError::SomeError @@ -44,22 +66,7 @@ fn compute_health_detail( // converts the token value to the basis token value for health computations // TODO: health basis token == USDC? - let oracle_data = &oracle_ai.try_borrow_data()?; - let oracle_type = determine_oracle_type(oracle_data)?; - require!(bank.oracle == oracle_ai.key(), MangoError::UnexpectedOracle); - - let price = match oracle_type { - OracleType::Stub => { - AccountLoader::<'_, StubOracle>::try_from(oracle_ai)? - .load()? - .price - } - OracleType::Pyth => { - let price_struct = load_price(&oracle_data).unwrap(); - I80F48::from_num(price_struct.agg.price) - } - }; - + let price = *price; let native_position = position.native(&bank); let native_basis = cm!(native_position * price); if native_basis.is_positive() { @@ -69,11 +76,21 @@ fn compute_health_detail( } } - // TODO: Serum open orders - // - for each active serum market, pass the OpenOrders in order - // - store the base_token_index and quote_token_index in the account, so we don't - // need to also pass SerumMarket - // - find the bank and oracle for base and quote, and add appropriately + // health contribution from serum accounts + for (serum_account, oo_ai) in + util::zip!(account.serum_account_map.iter_active(), serum_oos.iter()) + { + // This assumes serum open orders are passed in order + require!( + &serum_account.open_orders == oo_ai.key, + MangoError::SomeError + ); + + // find the prices for the market + // TODO: each of these is a linear scan through banks_and_prices - is that too expensive? + let _base_price = find_price(serum_account.base_token_index, &banks_and_prices)?; + let _quote_price = find_price(serum_account.quote_token_index, &banks_and_prices)?; + } Ok(cm!(assets - liabilities)) } diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index 80597b5d9..7d328b1b4 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -3,6 +3,7 @@ use anchor_lang::Discriminator; use fixed::types::I80F48; use crate::error::MangoError; +use crate::util::LoadZeroCopy; #[derive(PartialEq)] pub enum OracleType { @@ -26,6 +27,19 @@ pub fn determine_oracle_type(data: &[u8]) -> Result { Err(MangoError::UnknownOracleType.into()) } +pub fn oracle_price(acc_info: &AccountInfo) -> Result { + let data = &acc_info.try_borrow_data()?; + let oracle_type = determine_oracle_type(data)?; + + Ok(match oracle_type { + OracleType::Stub => acc_info.load::()?.price, + OracleType::Pyth => { + let price_struct = pyth_client::load_price(&data).unwrap(); + I80F48::from_num(price_struct.agg.price) + } + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/programs/mango-v4/src/util.rs b/programs/mango-v4/src/util.rs index e42703cd9..4a029e79a 100644 --- a/programs/mango-v4/src/util.rs +++ b/programs/mango-v4/src/util.rs @@ -1,3 +1,8 @@ +use anchor_lang::prelude::*; +use anchor_lang::ZeroCopy; +use arrayref::array_ref; +use std::{cell::Ref, mem}; + #[macro_export] macro_rules! zip { ($x: expr) => ($x); @@ -15,3 +20,44 @@ macro_rules! checked_math { }; } pub(crate) use checked_math; + +pub trait LoadZeroCopy { + /// Using AccountLoader forces a AccountInfo.clone() and then binds the loaded + /// lifetime to the AccountLoader's lifetime. This function avoids both. + /// It checks the account owner and discriminator, then casts the data. + fn load(&self) -> Result>; + + /// Same as load(), but doesn't check the discriminator. + fn load_unchecked(&self) -> Result>; +} + +impl<'info> LoadZeroCopy for AccountInfo<'info> { + fn load(&self) -> Result> { + if self.owner != &T::owner() { + return Err(ErrorCode::AccountOwnedByWrongProgram.into()); + } + + let data = self.try_borrow_data()?; + + let disc_bytes = array_ref![data, 0, 8]; + if disc_bytes != &T::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + + Ok(Ref::map(data, |data| { + bytemuck::from_bytes(&data[8..mem::size_of::() + 8]) + })) + } + + fn load_unchecked(&self) -> Result> { + if self.owner != &T::owner() { + return Err(ErrorCode::AccountOwnedByWrongProgram.into()); + } + + let data = self.try_borrow_data()?; + + Ok(Ref::map(data, |data| { + bytemuck::from_bytes(&data[8..mem::size_of::() + 8]) + })) + } +}