diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 1b04012c7..3177cca7c 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -39,6 +39,7 @@ pub fn new_health_cache_( let retriever = FixedOrderAccountRetriever { ais: accounts, n_banks: active_token_len, + n_perps: active_perp_len, begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len, }; diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index d22ea1dd2..134144f9e 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -49,8 +49,7 @@ pub fn perp_create_market( perp_market_index: PerpMarketIndex, name: String, oracle_config: OracleConfig, - base_token_index_opt: Option, - base_token_decimals: u8, + base_decimals: u8, quote_lot_size: i64, base_lot_size: i64, maint_asset_weight: f32, @@ -94,10 +93,10 @@ pub fn perp_create_market( fees_settled: I80F48::ZERO, // Why optional - Perp could be based purely on an oracle bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?, - base_token_decimals, + base_decimals, perp_market_index, - base_token_index: base_token_index_opt.ok_or(TokenIndex::MAX).unwrap(), registration_time: Clock::get()?.unix_timestamp, + padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), reserved: [0; 112], diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index ace01f5ba..420015d1c 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -22,8 +22,7 @@ pub fn perp_edit_market( ctx: Context, oracle_opt: Option, oracle_config_opt: Option, - base_token_index_opt: Option, - base_token_decimals_opt: Option, + base_decimals_opt: Option, maint_asset_weight_opt: Option, init_asset_weight_opt: Option, maint_liab_weight_opt: Option, @@ -100,17 +99,13 @@ pub fn perp_edit_market( // fees_accrued // bump - if let Some(base_token_decimals) = base_token_decimals_opt { - perp_market.base_token_decimals = base_token_decimals; + if let Some(base_decimals) = base_decimals_opt { + perp_market.base_decimals = base_decimals; } // unchanged - // perp_market_index - if let Some(base_token_index) = base_token_index_opt { - perp_market.base_token_index = base_token_index; - } - // unchanged - // quote_token_index diff --git a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs index 9807b692a..5baac5f33 100644 --- a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs @@ -22,6 +22,9 @@ pub struct PerpLiqForceCancelOrders<'info> { pub asks: AccountLoader<'info, BookSide>, #[account(mut)] pub bids: AccountLoader<'info, BookSide>, + + /// CHECK: Oracle can have different account types, constrained by address in perp_market + pub oracle: UncheckedAccount<'info>, } pub fn perp_liq_force_cancel_orders( diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 2844a89ae..eab9eb1c2 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -382,8 +382,7 @@ pub mod mango_v4 { perp_market_index: PerpMarketIndex, name: String, oracle_config: OracleConfig, - base_token_index_opt: Option, - base_token_decimals: u8, + base_decimals: u8, quote_lot_size: i64, base_lot_size: i64, maint_asset_weight: f32, @@ -402,8 +401,7 @@ pub mod mango_v4 { perp_market_index, name, oracle_config, - base_token_index_opt, - base_token_decimals, + base_decimals, quote_lot_size, base_lot_size, maint_asset_weight, @@ -424,8 +422,7 @@ pub mod mango_v4 { ctx: Context, oracle_opt: Option, oracle_config_opt: Option, - base_token_index_opt: Option, - base_token_decimals_opt: Option, + base_decimals_opt: Option, maint_asset_weight_opt: Option, init_asset_weight_opt: Option, maint_liab_weight_opt: Option, @@ -441,8 +438,7 @@ pub mod mango_v4 { ctx, oracle_opt, oracle_config_opt, - base_token_index_opt, - base_token_decimals_opt, + base_decimals_opt, maint_asset_weight_opt, init_asset_weight_opt, maint_liab_weight_opt, diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index c11edf072..cd70de393 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -41,12 +41,12 @@ pub trait AccountRetriever { fn serum_oo(&self, account_index: usize, key: &Pubkey) -> Result<&OpenOrders>; - fn perp_market( + fn perp_market_and_oracle_price( &self, group: &Pubkey, account_index: usize, perp_market_index: PerpMarketIndex, - ) -> Result<&PerpMarket>; + ) -> Result<(&PerpMarket, I80F48)>; } /// Assumes the account infos needed for the health computation follow a strict order. @@ -54,10 +54,12 @@ pub trait AccountRetriever { /// 1. n_banks Bank account, in the order of account.token_iter_active() /// 2. n_banks oracle accounts, one for each bank in the same order /// 3. PerpMarket accounts, in the order of account.perps.iter_active_accounts() -/// 4. serum3 OpenOrders accounts, in the order of account.serum3.iter_active() +/// 4. PerpMarket oracle accounts, in the order of the perp market accounts +/// 5. serum3 OpenOrders accounts, in the order of account.serum3.iter_active() pub struct FixedOrderAccountRetriever { pub ais: Vec, pub n_banks: usize, + pub n_perps: usize, pub begin_perp: usize, pub begin_serum3: usize, } @@ -70,15 +72,16 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( let active_serum3_len = account.active_serum3_orders().count(); let active_perp_len = account.active_perp_positions().count(); let expected_ais = cm!(active_token_len * 2 // banks + oracles - + active_perp_len // PerpMarkets + + active_perp_len * 2 // PerpMarkets + Oracles + active_serum3_len); // open_orders require_eq!(ais.len(), expected_ais); Ok(FixedOrderAccountRetriever { ais: AccountInfoRef::borrow_slice(ais)?, n_banks: active_token_len, + n_perps: active_perp_len, begin_perp: cm!(active_token_len * 2), - begin_serum3: cm!(active_token_len * 2 + active_perp_len), + begin_serum3: cm!(active_token_len * 2 + active_perp_len * 2), }) } @@ -90,10 +93,28 @@ impl FixedOrderAccountRetriever { Ok(bank) } + fn perp_market( + &self, + group: &Pubkey, + account_index: usize, + perp_market_index: PerpMarketIndex, + ) -> Result<&PerpMarket> { + let market_ai = &self.ais[self.begin_perp + account_index]; + let market = market_ai.load::()?; + require_keys_eq!(market.group, *group); + require_eq!(market.perp_market_index, perp_market_index); + Ok(market) + } + fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result { let oracle = &self.ais[cm!(self.n_banks + account_index)]; bank.oracle_price(oracle) } + + fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result { + let oracle = &self.ais[self.begin_perp + self.n_perps + account_index]; + perp_market.oracle_price(oracle) + } } impl AccountRetriever for FixedOrderAccountRetriever { @@ -126,27 +147,32 @@ impl AccountRetriever for FixedOrderAccountRetriever { Ok((bank, oracle_price)) } - fn perp_market( + fn perp_market_and_oracle_price( &self, group: &Pubkey, account_index: usize, perp_market_index: PerpMarketIndex, - ) -> Result<&PerpMarket> { - let ai = &self.ais[cm!(self.begin_perp + account_index)]; - (|| { - let market = ai.load::()?; - require_keys_eq!(market.group, *group); - require_eq!(market.perp_market_index, perp_market_index); - Ok(market) - })() - .with_context(|| { + ) -> Result<(&PerpMarket, I80F48)> { + let perp_market = self + .perp_market(group, account_index, perp_market_index) + .with_context(|| { + format!( + "loading perp market with health account index {} and perp market index {}, passed account {}", + account_index, + perp_market_index, + self.ais[self.begin_perp + account_index].key(), + ) + })?; + + let oracle_price = self.oracle_price_perp(account_index, perp_market).with_context(|| { format!( - "loading perp market with health account index {} and perp market index {}, passed account {}", + "getting oracle for perp market with health account index {} and perp market index {}, passed account {}", account_index, perp_market_index, - ai.key(), + self.ais[self.begin_perp + self.n_perps + account_index].key(), ) - }) + })?; + Ok((perp_market, oracle_price)) } fn serum_oo(&self, account_index: usize, key: &Pubkey) -> Result<&OpenOrders> { @@ -169,6 +195,7 @@ impl AccountRetriever for FixedOrderAccountRetriever { /// - an unknown number of Banks in any order, followed by /// - the same number of oracles in the same order as the banks, followed by /// - an unknown number of PerpMarket accounts +/// - the same number of oracles in the same order as the perp markets /// - an unknown number of serum3 OpenOrders accounts /// and retrieves accounts needed for the health computation by doing a linear /// scan for each request. @@ -176,6 +203,7 @@ pub struct ScanningAccountRetriever<'a, 'info> { banks: Vec>, oracles: Vec>, perp_markets: Vec>, + perp_oracles: Vec>, serum3_oos: Vec>, token_index_map: HashMap, perp_index_map: HashMap, @@ -239,13 +267,16 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { }) })?; let n_perps = perp_index_map.len(); - - let serum3_start = perps_start + n_perps; + let perp_oracles_start = perps_start + n_perps; + let serum3_start = perp_oracles_start + n_perps; Ok(Self { banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?, oracles: AccountInfoRef::borrow_slice(&ais[n_banks..2 * n_banks])?, perp_markets: AccountInfoRef::borrow_slice(&ais[perps_start..perps_start + n_perps])?, + perp_oracles: AccountInfoRef::borrow_slice( + &ais[perp_oracles_start..perp_oracles_start + n_perps], + )?, serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?, token_index_map, perp_index_map, @@ -312,9 +343,15 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { Ok((bank, bank.oracle_price(oracle)?)) } - pub fn scanned_perp_market(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpMarket> { + pub fn scanned_perp_market_and_oracle( + &self, + perp_market_index: PerpMarketIndex, + ) -> Result<(&PerpMarket, I80F48)> { let index = self.perp_market_index(perp_market_index)?; - self.perp_markets[index].load_fully_unchecked::() + let perp_market = self.perp_markets[index].load_fully_unchecked::()?; + let oracle_acc = &self.perp_oracles[index]; + let oracle_price = perp_market.oracle_price(oracle_acc)?; + Ok((perp_market, oracle_price)) } pub fn scanned_serum_oo(&self, key: &Pubkey) -> Result<&OpenOrders> { @@ -337,13 +374,13 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> { self.scanned_bank_and_oracle(token_index) } - fn perp_market( + fn perp_market_and_oracle_price( &self, _group: &Pubkey, _account_index: usize, perp_market_index: PerpMarketIndex, - ) -> Result<&PerpMarket> { - self.scanned_perp_market(perp_market_index) + ) -> Result<(&PerpMarket, I80F48)> { + self.scanned_perp_market_and_oracle(perp_market_index) } fn serum_oo(&self, _account_index: usize, key: &Pubkey) -> Result<&OpenOrders> { @@ -484,19 +521,15 @@ pub struct PerpInfo { pub base: I80F48, // in health-reference-token native units, no asset/liab factor needed pub quote: I80F48, + oracle_price: I80F48, } impl PerpInfo { fn new( perp_position: &PerpPosition, perp_market: &PerpMarket, - token_infos: &[TokenInfo], + oracle_price: I80F48, ) -> Result { - // find the TokenInfos for the market's base and quote tokens - let base_index = find_token_info_index(token_infos, perp_market.base_token_index)?; - // TODO: base_index could be unset - let base_info = &token_infos[base_index]; - let base_lot_size = I80F48::from(perp_market.base_lot_size); let base_lots = cm!(perp_position.base_position_lots() + perp_position.taker_base_lots); @@ -550,7 +583,7 @@ impl PerpInfo { let bids_net_lots = cm!(base_lots + perp_position.bids_base_lots); let asks_net_lots = cm!(base_lots - perp_position.asks_base_lots); - let lots_to_quote = base_lot_size * base_info.oracle_price; + let lots_to_quote = base_lot_size * oracle_price; let base; let quote; if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) { @@ -573,6 +606,7 @@ impl PerpInfo { maint_liab_weight: perp_market.maint_liab_weight, base, quote, + oracle_price, }) } @@ -723,7 +757,7 @@ impl HealthCache { .iter_mut() .find(|m| m.perp_market_index == perp_market.perp_market_index) .ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?; - *perp_entry = PerpInfo::new(perp_position, perp_market, &self.token_infos)?; + *perp_entry = PerpInfo::new(perp_position, perp_market, perp_entry.oracle_price)?; Ok(()) } @@ -1066,9 +1100,12 @@ pub fn new_health_cache( // health contribution from perp accounts let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count()); for (i, perp_position) in account.active_perp_positions().enumerate() { - let perp_market = - retriever.perp_market(&account.fixed.group, i, perp_position.market_index)?; - perp_infos.push(PerpInfo::new(perp_position, perp_market, &token_infos)?); + let (perp_market, oracle_price) = retriever.perp_market_and_oracle_price( + &account.fixed.group, + i, + perp_position.market_index, + )?; + perp_infos.push(PerpInfo::new(perp_position, perp_market, oracle_price)?); } Ok(HealthCache { @@ -1201,15 +1238,15 @@ mod tests { fn mock_perp_market( group: Pubkey, + oracle: Pubkey, market_index: PerpMarketIndex, - base_token: TokenIndex, init_weights: f64, maint_weights: f64, ) -> TestAccount { let mut pm = TestAccount::::new_zeroed(); pm.data().group = group; + pm.data().oracle = oracle; pm.data().perp_market_index = market_index; - pm.data().base_token_index = base_token; pm.data().init_asset_weight = I80F48::from_num(1.0 - init_weights); pm.data().init_liab_weight = I80F48::from_num(1.0 + init_weights); pm.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights); @@ -1264,7 +1301,7 @@ mod tests { oo1.data().native_coin_free = 3; oo1.data().referrer_rebates_accrued = 2; - let mut perp1 = mock_perp_market(group, 9, 4, 0.2, 0.1); + let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1); let perpaccount = account.ensure_perp_position(9).unwrap().0; perpaccount.change_base_and_quote_positions(perp1.data(), 3, -I80F48::from(310u16)); perpaccount.bids_base_lots = 7; @@ -1272,12 +1309,15 @@ mod tests { perpaccount.taker_base_lots = 1; perpaccount.taker_quote_lots = 2; + let oracle2_ai = oracle2.as_account_info(); + let ais = vec![ bank1.as_account_info(), bank2.as_account_info(), oracle1.as_account_info(), - oracle2.as_account_info(), + oracle2_ai.clone(), perp1.as_account_info(), + oracle2_ai, oo1.as_account_info(), ]; @@ -1298,10 +1338,12 @@ mod tests { #[test] fn test_scanning_account_retriever() { + let oracle1_price = 1.0; + let oracle2_price = 5.0; let group = Pubkey::new_unique(); - let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1); - let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3); + let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, oracle1_price, 0.2, 0.1); + let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, oracle2_price, 0.5, 0.3); let (mut bank3, _) = mock_bank_and_oracle(group, 5, 1.0, 0.5, 0.3); // bank3 reuses the bank2 oracle, to ensure the ScanningAccountRetriever doesn't choke on that @@ -1311,17 +1353,22 @@ mod tests { let oo1key = oo1.pubkey; oo1.data().native_pc_total = 20; - let mut perp1 = mock_perp_market(group, 9, 4, 0.2, 0.1); + let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1); + let mut perp2 = mock_perp_market(group, oracle1.pubkey, 8, 0.2, 0.1); + let oracle1_account_info = oracle1.as_account_info(); let oracle2_account_info = oracle2.as_account_info(); let ais = vec![ bank1.as_account_info(), bank2.as_account_info(), bank3.as_account_info(), - oracle1.as_account_info(), + oracle1_account_info.clone(), + oracle2_account_info.clone(), oracle2_account_info.clone(), - oracle2_account_info, perp1.as_account_info(), + perp2.as_account_info(), + oracle2_account_info, + oracle1_account_info, oo1.as_account_info(), ]; @@ -1330,8 +1377,9 @@ mod tests { assert_eq!(retriever.banks.len(), 3); assert_eq!(retriever.token_index_map.len(), 3); assert_eq!(retriever.oracles.len(), 3); - assert_eq!(retriever.perp_markets.len(), 1); - assert_eq!(retriever.perp_index_map.len(), 1); + assert_eq!(retriever.perp_markets.len(), 2); + assert_eq!(retriever.perp_oracles.len(), 2); + assert_eq!(retriever.perp_index_map.len(), 2); assert_eq!(retriever.serum3_oos.len(), 1); { @@ -1372,10 +1420,21 @@ mod tests { assert!(retriever.serum_oo(1, &Pubkey::default()).is_err()); - let perp = retriever.perp_market(&group, 0, 9).unwrap(); + let (perp, oracle_price) = retriever + .perp_market_and_oracle_price(&group, 0, 9) + .unwrap(); assert_eq!(identity(perp.perp_market_index), 9); + assert_eq!(oracle_price, oracle2_price); - assert!(retriever.perp_market(&group, 1, 5).is_err()); + let (perp, oracle_price) = retriever + .perp_market_and_oracle_price(&group, 1, 8) + .unwrap(); + assert_eq!(identity(perp.perp_market_index), 8); + assert_eq!(oracle_price, oracle1_price); + + assert!(retriever + .perp_market_and_oracle_price(&group, 1, 5) + .is_err()); } #[derive(Default)] @@ -1435,7 +1494,7 @@ mod tests { oo2.data().native_pc_total = testcase.oo_1_3.0; oo2.data().native_coin_total = testcase.oo_1_3.1; - let mut perp1 = mock_perp_market(group, 9, 4, 0.2, 0.1); + let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1); let perpaccount = account.ensure_perp_position(9).unwrap().0; perpaccount.change_base_and_quote_positions( perp1.data(), @@ -1445,14 +1504,16 @@ mod tests { perpaccount.bids_base_lots = testcase.perp1.2; perpaccount.asks_base_lots = testcase.perp1.3; + let oracle2_ai = oracle2.as_account_info(); let ais = vec![ bank1.as_account_info(), bank2.as_account_info(), bank3.as_account_info(), oracle1.as_account_info(), - oracle2.as_account_info(), + oracle2_ai.clone(), oracle3.as_account_info(), perp1.as_account_info(), + oracle2_ai, oo1.as_account_info(), oo2.as_account_info(), ]; @@ -1785,16 +1846,18 @@ mod tests { ) .unwrap(); - let mut perp1 = mock_perp_market(group, 9, 1, 0.2, 0.1); + let mut perp1 = mock_perp_market(group, oracle1.pubkey, 9, 0.2, 0.1); perp1.data().long_funding = I80F48::from_num(10.1); let perpaccount = account.ensure_perp_position(9).unwrap().0; perpaccount.change_base_and_quote_positions(perp1.data(), 10, I80F48::from(-110)); perpaccount.long_settled_funding = I80F48::from_num(10.0); + let oracle1_ai = oracle1.as_account_info(); let ais = vec![ bank1.as_account_info(), - oracle1.as_account_info(), + oracle1_ai.clone(), perp1.as_account_info(), + oracle1_ai, ]; let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap(); @@ -1811,4 +1874,35 @@ mod tests { - 1.0 )); } + + #[test] + fn test_scanning_retreiver_mismatched_oracle_for_perps_throws_error() { + let group = Pubkey::new_unique(); + + let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1); + let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3); + + let mut oo1 = TestAccount::::new_zeroed(); + + let mut perp1 = mock_perp_market(group, oracle1.pubkey, 9, 0.2, 0.1); + let mut perp2 = mock_perp_market(group, oracle2.pubkey, 8, 0.2, 0.1); + + let oracle1_account_info = oracle1.as_account_info(); + let oracle2_account_info = oracle2.as_account_info(); + let ais = vec![ + bank1.as_account_info(), + bank2.as_account_info(), + oracle1_account_info.clone(), + oracle2_account_info.clone(), + perp1.as_account_info(), + perp2.as_account_info(), + oracle2_account_info, // Oracles wrong way around + oracle1_account_info, + oo1.as_account_info(), + ]; + + let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap(); + let result = retriever.perp_market_and_oracle_price(&group, 0, 9); + assert!(result.is_err()); + } } diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 204026f5c..89a27a9f7 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -419,7 +419,6 @@ mod tests { fn create_perp_market() -> PerpMarket { return PerpMarket { group: Pubkey::new_unique(), - base_token_index: 0, perp_market_index: 0, name: Default::default(), oracle: Pubkey::new_unique(), @@ -449,8 +448,9 @@ mod tests { fees_accrued: I80F48::ZERO, fees_settled: I80F48::ZERO, bump: 0, - base_token_decimals: 0, + base_decimals: 0, reserved: [0; 112], + padding0: Default::default(), padding1: Default::default(), padding2: Default::default(), registration_time: 0, diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index 08f8b7556..5db22d248 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -116,7 +116,7 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result Result { let data = &acc_info.data(); let oracle_type = determine_oracle_type(acc_info)?; @@ -142,8 +142,7 @@ pub fn oracle_price( return Err(MangoError::SomeError.into()); } - let decimals = - cm!((price_account.expo as i8) + QUOTE_DECIMALS - (base_token_decimals as i8)); + let decimals = cm!((price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8)); let decimal_adj = power_of_ten(decimals); cm!(price * decimal_adj) } @@ -173,7 +172,7 @@ pub fn oracle_price( return Err(MangoError::SomeError.into()); } - let decimals = cm!(QUOTE_DECIMALS - (base_token_decimals as i8)); + let decimals = cm!(QUOTE_DECIMALS - (base_decimals as i8)); let decimal_adj = power_of_ten(decimals); cm!(price * decimal_adj) } @@ -195,7 +194,7 @@ pub fn oracle_price( return Err(MangoError::SomeError.into()); } - let decimals = cm!(QUOTE_DECIMALS - (base_token_decimals as i8)); + let decimals = cm!(QUOTE_DECIMALS - (base_decimals as i8)); let decimal_adj = power_of_ten(decimals); cm!(price * decimal_adj) } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 0b0c63ee1..a79b54ed9 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -6,8 +6,8 @@ use fixed::types::I80F48; use static_assertions::const_assert_eq; use crate::accounts_zerocopy::KeyedAccountReader; +use crate::state::oracle; use crate::state::orderbook::order_type::Side; -use crate::state::{oracle, TokenIndex}; use crate::util::checked_math as cm; use super::{Book, OracleConfig, DAY_I80F48}; @@ -20,9 +20,8 @@ pub struct PerpMarket { // ABI: Clients rely on this being at offset 8 pub group: Pubkey, - // TODO: Remove! // ABI: Clients rely on this being at offset 40 - pub base_token_index: TokenIndex, + pub padding0: [u8; 2], /// Lookup indices pub perp_market_index: PerpMarketIndex, @@ -85,7 +84,7 @@ pub struct PerpMarket { /// PDA bump pub bump: u8, - pub base_token_decimals: u8, + pub base_decimals: u8, pub padding2: [u8; 6], @@ -125,7 +124,7 @@ impl PerpMarket { oracle::oracle_price( oracle_acc, self.oracle_config.conf_filter, - self.base_token_decimals, + self.base_decimals, ) } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 060e1bae3..2baafbe95 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -166,6 +166,14 @@ fn get_perp_market_address_by_index(group: Pubkey, perp_market_index: PerpMarket .0 } +async fn get_oracle_address_from_perp_market_address( + account_loader: &impl ClientAccountLoader, + perp_market_address: &Pubkey, +) -> Pubkey { + let perp_market: PerpMarket = account_loader.load(&perp_market_address).await.unwrap(); + perp_market.oracle +} + // all the accounts that instructions like deposit/withdraw need to compute account health async fn derive_health_check_remaining_account_metas( account_loader: &impl ClientAccountLoader, @@ -201,6 +209,14 @@ async fn derive_health_check_remaining_account_metas( .active_perp_positions() .map(|perp| get_perp_market_address_by_index(account.fixed.group, perp.market_index)); + let mut perp_oracles = vec![]; + for perp in adjusted_account + .active_perp_positions() + .map(|perp| get_perp_market_address_by_index(account.fixed.group, perp.market_index)) + { + perp_oracles.push(get_oracle_address_from_perp_market_address(account_loader, &perp).await) + } + let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders); let to_account_meta = |pubkey| AccountMeta { @@ -218,6 +234,7 @@ async fn derive_health_check_remaining_account_metas( }) .chain(oracles.into_iter().map(to_account_meta)) .chain(perp_markets.map(to_account_meta)) + .chain(perp_oracles.into_iter().map(to_account_meta)) .chain(serum_oos.map(to_account_meta)) .collect() } @@ -251,11 +268,17 @@ async fn derive_liquidation_remaining_account_metas( oracles.push(mint_info.oracle); } - let perp_markets = liqee + let perp_markets: Vec = liqee .active_perp_positions() .chain(liqee.active_perp_positions()) .map(|perp| get_perp_market_address_by_index(liqee.fixed.group, perp.market_index)) - .unique(); + .unique() + .collect(); + + let mut perp_oracles = vec![]; + for &perp in &perp_markets { + perp_oracles.push(get_oracle_address_from_perp_market_address(account_loader, &perp).await) + } let serum_oos = liqee .active_serum3_orders() @@ -276,7 +299,8 @@ async fn derive_liquidation_remaining_account_metas( is_signer: false, }) .chain(oracles.into_iter().map(to_account_meta)) - .chain(perp_markets.map(to_account_meta)) + .chain(perp_markets.into_iter().map(to_account_meta)) + .chain(perp_oracles.into_iter().map(to_account_meta)) .chain(serum_oos.map(to_account_meta)) .collect() } @@ -2107,8 +2131,7 @@ pub struct PerpCreateMarketInstruction { pub event_queue: Pubkey, pub payer: TestKeypair, pub perp_market_index: PerpMarketIndex, - pub base_token_index: TokenIndex, - pub base_token_decimals: u8, + pub base_decimals: u8, pub quote_lot_size: i64, pub base_lot_size: i64, pub maint_asset_weight: f32, @@ -2135,8 +2158,7 @@ impl PerpCreateMarketInstruction { .create_account_for_type::(&mango_v4::id()) .await, oracle: base.oracle, - base_token_index: base.index, - base_token_decimals: base.mint.decimals, + base_decimals: base.mint.decimals, ..PerpCreateMarketInstruction::default() } } @@ -2156,7 +2178,6 @@ impl ClientInstruction for PerpCreateMarketInstruction { conf_filter: I80F48::from_num::(0.10), }, perp_market_index: self.perp_market_index, - base_token_index_opt: Option::from(self.base_token_index), quote_lot_size: self.quote_lot_size, base_lot_size: self.base_lot_size, maint_asset_weight: self.maint_asset_weight, @@ -2169,7 +2190,7 @@ impl ClientInstruction for PerpCreateMarketInstruction { max_funding: 0.05, min_funding: 0.05, impact_quantity: 100, - base_token_decimals: self.base_token_decimals, + base_decimals: self.base_decimals, }; let perp_market = Pubkey::find_program_address( @@ -2666,6 +2687,7 @@ impl ClientInstruction for PerpLiqForceCancelOrdersInstruction { account: self.account, bids: perp_market.bids, asks: perp_market.asks, + oracle: perp_market.oracle, }; let mut instruction = make_instruction(program_id, &accounts, instruction); instruction.accounts.extend(health_check_metas); diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index 2e88f75a2..0cecf12b8 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -221,10 +221,6 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { liquidation_fee: 0.012, maker_fee: 0.0002, taker_fee: 0.000, - // HACK: Currently the base_token_index token needs to be active on the account. - // Using token[0] for each market allows us to have multiple perp positions with - // just a single token position. - base_token_index: tokens[0].index, ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &token).await }, ) diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 8ed30b75c..c513d9b8c 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -31,7 +31,6 @@ export class PerpMarket { publicKey: PublicKey, obj: { group: PublicKey; - baseTokenIndex: number; quoteTokenIndex: number; perpMarketIndex: number; name: number[]; @@ -59,14 +58,13 @@ export class PerpMarket { seqNum: any; // TODO: ts complains that this is unknown for whatever reason feesAccrued: I80F48Dto; bump: number; - baseTokenDecimals: number; + baseDecimals: number; registrationTime: BN; }, ): PerpMarket { return new PerpMarket( publicKey, obj.group, - obj.baseTokenIndex, obj.quoteTokenIndex, obj.perpMarketIndex, obj.name, @@ -94,7 +92,7 @@ export class PerpMarket { obj.seqNum, obj.feesAccrued, obj.bump, - obj.baseTokenDecimals, + obj.baseDecimals, obj.registrationTime, ); } @@ -102,7 +100,6 @@ export class PerpMarket { constructor( public publicKey: PublicKey, public group: PublicKey, - public baseTokenIndex: number, public quoteTokenIndex: number, public perpMarketIndex: number, name: number[], @@ -130,7 +127,7 @@ export class PerpMarket { seqNum: BN, feesAccrued: I80F48Dto, bump: number, - public baseTokenDecimals: number, + public baseDecimals: number, public registrationTime: BN, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 0f384516c..844f85e16 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1273,8 +1273,7 @@ export class MangoClient { perpMarketIndex: number, name: string, oracleConfFilter: number, - baseTokenIndex: number, - baseTokenDecimals: number, + baseDecimals: number, quoteTokenIndex: number, quoteLotSize: number, baseLotSize: number, @@ -1302,8 +1301,7 @@ export class MangoClient { val: I80F48.fromNumber(oracleConfFilter).getData(), }, } as any, // future: nested custom types dont typecheck, fix if possible? - baseTokenIndex, - baseTokenDecimals, + baseDecimals, new BN(quoteLotSize), new BN(baseLotSize), maintAssetWeight, @@ -1374,8 +1372,7 @@ export class MangoClient { perpMarketName: string, oracle: PublicKey, oracleConfFilter: number, - baseTokenIndex: number, - baseTokenDecimals: number, + baseDecimals: number, maintAssetWeight: number, initAssetWeight: number, maintLiabWeight: number, @@ -1397,8 +1394,7 @@ export class MangoClient { val: I80F48.fromNumber(oracleConfFilter).getData(), }, } as any, // future: nested custom types dont typecheck, fix if possible? - baseTokenIndex, - baseTokenDecimals, + baseDecimals, maintAssetWeight, initAssetWeight, maintLiabWeight, @@ -1439,10 +1435,7 @@ export class MangoClient { .rpc(); } - public async perpGetMarkets( - group: Group, - baseTokenIndex?: number, - ): Promise { + public async perpGetMarkets(group: Group): Promise { const bumpfbuf = Buffer.alloc(1); bumpfbuf.writeUInt8(255); @@ -1455,17 +1448,6 @@ export class MangoClient { }, ]; - if (baseTokenIndex) { - const bbuf = Buffer.alloc(2); - bbuf.writeUInt16LE(baseTokenIndex); - filters.push({ - memcmp: { - bytes: bs58.encode(bbuf), - offset: 40, - }, - }); - } - return (await this.program.account.perpMarket.all(filters)).map((tuple) => PerpMarket.from(tuple.publicKey, tuple.account), ); @@ -1910,6 +1892,18 @@ export class MangoClient { )[0].publicKey, ), ); + + healthRemainingAccounts.push( + ...mangoAccount.perps + .filter((perp) => perp.marketIndex !== 65535) + .map( + (perp) => + Array.from(group.perpMarketsMap.values()).filter( + (perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex, + )[0].oracle, + ), + ); + for (const perpMarket of perpMarkets) { const alreadyAdded = mangoAccount.perps.find( (p) => p.marketIndex === perpMarket.perpMarketIndex, @@ -1968,23 +1962,17 @@ export class MangoClient { ...mintInfos.map((mintInfo) => mintInfo.oracle), ); - for (const mangoAccount of mangoAccounts) { - healthRemainingAccounts.push( - ...mangoAccount.serum3 - .filter((serum3Account) => serum3Account.marketIndex !== 65535) - .map((serum3Account) => serum3Account.openOrders), - ); - } + const perpsToAdd: PerpMarket[] = []; for (const mangoAccount of mangoAccounts) { - healthRemainingAccounts.push( + perpsToAdd.push( ...mangoAccount.perps .filter((perp) => perp.marketIndex !== 65535) .map( (perp) => Array.from(group.perpMarketsMap.values()).filter( (perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex, - )[0].publicKey, + )[0], ), ); } @@ -1994,15 +1982,28 @@ export class MangoClient { (p) => p.marketIndex === perpMarket.perpMarketIndex, ); if (!alreadyAdded) { - healthRemainingAccounts.push( + perpsToAdd.push( Array.from(group.perpMarketsMap.values()).filter( (p) => p.perpMarketIndex === perpMarket.perpMarketIndex, - )[0].publicKey, + )[0], ); } } } + // Add perp accounts + healthRemainingAccounts.push(...perpsToAdd.map((p) => p.publicKey)); + // Add oracle for each perp + healthRemainingAccounts.push(...perpsToAdd.map((p) => p.oracle)); + + for (const mangoAccount of mangoAccounts) { + healthRemainingAccounts.push( + ...mangoAccount.serum3 + .filter((serum3Account) => serum3Account.marketIndex !== 65535) + .map((serum3Account) => serum3Account.openOrders), + ); + } + return healthRemainingAccounts; } } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 91c30b412..71f17f6b2 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -2257,13 +2257,7 @@ export type MangoV4 = { } }, { - "name": "baseTokenIndexOpt", - "type": { - "option": "u16" - } - }, - { - "name": "baseTokenDecimals", + "name": "baseDecimals", "type": "u8" }, { @@ -2351,13 +2345,7 @@ export type MangoV4 = { } }, { - "name": "baseTokenIndexOpt", - "type": { - "option": "u16" - } - }, - { - "name": "baseTokenDecimalsOpt", + "name": "baseDecimalsOpt", "type": { "option": "u8" } @@ -2953,6 +2941,11 @@ export type MangoV4 = { "name": "bids", "isMut": true, "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -3726,8 +3719,13 @@ export type MangoV4 = { "type": "publicKey" }, { - "name": "baseTokenIndex", - "type": "u16" + "name": "padding0", + "type": { + "array": [ + "u8", + 2 + ] + } }, { "name": "perpMarketIndex", @@ -3901,7 +3899,7 @@ export type MangoV4 = { "type": "u8" }, { - "name": "baseTokenDecimals", + "name": "baseDecimals", "type": "u8" }, { @@ -4293,6 +4291,12 @@ export type MangoV4 = { "type": { "defined": "I80F48" } + }, + { + "name": "oraclePrice", + "type": { + "defined": "I80F48" + } } ] } @@ -8256,13 +8260,7 @@ export const IDL: MangoV4 = { } }, { - "name": "baseTokenIndexOpt", - "type": { - "option": "u16" - } - }, - { - "name": "baseTokenDecimals", + "name": "baseDecimals", "type": "u8" }, { @@ -8350,13 +8348,7 @@ export const IDL: MangoV4 = { } }, { - "name": "baseTokenIndexOpt", - "type": { - "option": "u16" - } - }, - { - "name": "baseTokenDecimalsOpt", + "name": "baseDecimalsOpt", "type": { "option": "u8" } @@ -8952,6 +8944,11 @@ export const IDL: MangoV4 = { "name": "bids", "isMut": true, "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -9725,8 +9722,13 @@ export const IDL: MangoV4 = { "type": "publicKey" }, { - "name": "baseTokenIndex", - "type": "u16" + "name": "padding0", + "type": { + "array": [ + "u8", + 2 + ] + } }, { "name": "perpMarketIndex", @@ -9900,7 +9902,7 @@ export const IDL: MangoV4 = { "type": "u8" }, { - "name": "baseTokenDecimals", + "name": "baseDecimals", "type": "u8" }, { @@ -10292,6 +10294,12 @@ export const IDL: MangoV4 = { "type": { "defined": "I80F48" } + }, + { + "name": "oraclePrice", + "type": { + "defined": "I80F48" + } } ] }