diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index c10079b52..c0395dd99 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -44,7 +44,7 @@ pub fn liq_token_with_token( require!(liqee.is_bankrupt == 0, MangoError::IsBankrupt); // Initial liqee health check - let mut liqee_health_cache = health_cache_for_liqee(&liqee, &account_retriever)?; + let mut liqee_health_cache = new_health_cache(&liqee, &account_retriever)?; let init_health = liqee_health_cache.health(HealthType::Init)?; if liqee.being_liquidated != 0 { if init_health > I80F48::ZERO { diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 32fb64afb..1283f90c2 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -289,7 +289,7 @@ pub fn compute_health_from_fixed_accounts( begin_perp: cm!(active_token_len * 2), begin_serum3: cm!(active_token_len * 2 + active_perp_len), }; - compute_health_detail(account, &retriever, health_type, true)?.health(health_type) + new_health_cache(account, &retriever)?.health(health_type) } /// Compute health with an arbitrary AccountRetriever @@ -298,21 +298,7 @@ pub fn compute_health( health_type: HealthType, retriever: &impl AccountRetriever, ) -> Result { - compute_health_detail(account, retriever, health_type, true)?.health(health_type) -} - -/// Compute health for a liqee. -/// -/// This has the advantage of returning a HealthCache, allowing for health -/// to be recomputed after token balance changes due to liquidation. -/// -/// However, this only works if the serum3 open orders accounts have been -/// fully settled (like via serum3_liq_force_cancel_orders). -pub fn health_cache_for_liqee( - account: &MangoAccount, - retriever: &impl AccountRetriever, -) -> Result { - compute_health_detail(account, retriever, HealthType::Init, false) + new_health_cache(account, retriever)?.health(health_type) } struct TokenInfo { @@ -324,6 +310,8 @@ struct TokenInfo { oracle_price: I80F48, // native/native // in health-reference-token native units balance: I80F48, + // in health-reference-token native units + serum3_max_reserved: I80F48, } impl TokenInfo { @@ -344,6 +332,42 @@ impl TokenInfo { } } +struct Serum3Info { + reserved: I80F48, + base_index: usize, + quote_index: usize, +} + +impl Serum3Info { + #[inline(always)] + fn health_contribution(&self, health_type: HealthType, token_infos: &[TokenInfo]) -> I80F48 { + let base_info = &token_infos[self.base_index]; + let quote_info = &token_infos[self.quote_index]; + let reserved = self.reserved; + + // compute how much the health would increase if the reserved balance were + // applied to the passed token info + let compute_health_effect = |token_info: &TokenInfo| { + let token_balance = cm!(token_info.balance + token_info.serum3_max_reserved); + let (asset_part, liab_part) = if token_balance >= reserved { + (reserved, I80F48::ZERO) + } else if token_balance.is_negative() { + (I80F48::ZERO, reserved) + } else { + (token_balance, cm!(reserved - token_balance)) + }; + + let asset_weight = token_info.asset_weight(health_type); + let liab_weight = token_info.liab_weight(health_type); + cm!(asset_weight * asset_part + liab_weight * liab_part) + }; + + let reserved_as_base = compute_health_effect(base_info); + let reserved_as_quote = compute_health_effect(quote_info); + reserved_as_base.min(reserved_as_quote) + } +} + struct PerpInfo { maint_asset_weight: I80F48, init_asset_weight: I80F48, @@ -384,6 +408,7 @@ impl PerpInfo { pub struct HealthCache { token_infos: Vec, + serum3_infos: Vec, perp_infos: Vec, } @@ -394,6 +419,10 @@ impl HealthCache { let contrib = health_contribution(health_type, token_info, token_info.balance)?; health = cm!(health + contrib); } + for serum3_info in self.serum3_infos.iter() { + let contrib = serum3_info.health_contribution(health_type, &self.token_infos); + health = cm!(health + contrib); + } for perp_info in self.perp_infos.iter() { let contrib = perp_info.health_contribution(health_type); health = cm!(health + contrib); @@ -429,29 +458,10 @@ fn health_contribution( Ok(cm!(balance * weight)) } -/// Compute health contribution of two tokens - pure convenience -#[inline(always)] -fn pair_health( - health_type: HealthType, - info1: &TokenInfo, - balance1: I80F48, - info2: &TokenInfo, -) -> Result { - let health1 = health_contribution(health_type, info1, balance1)?; - let health2 = health_contribution(health_type, info2, info2.balance)?; - Ok(cm!(health1 + health2)) -} - -/// The HealthInfo returned from this function is specialized for the health_type -/// unless called with allow_serum3=false. -/// -/// The reason is that the health type used can affect the way funds reserved for -/// orders get distributed to the token balances. -fn compute_health_detail( +/// Generate a HealthCache for an account and its health accounts. +pub fn new_health_cache( account: &MangoAccount, retriever: &impl AccountRetriever, - health_type: HealthType, - allow_serum3: bool, ) -> Result { // token contribution from token accounts let mut token_infos = vec![]; @@ -478,21 +488,15 @@ fn compute_health_detail( init_liab_weight: bank.init_liab_weight, oracle_price, balance: cm!(native * oracle_price), + serum3_max_reserved: I80F48::ZERO, }); } - // token contribution from serum accounts + // Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill + // the serum3_max_reserved with their reserved funds. Also build Serum3Infos. + let mut serum3_infos = vec![]; for (i, serum_account) in account.serum3.iter_active().enumerate() { let oo = retriever.serum_oo(i, &serum_account.open_orders)?; - if !allow_serum3 { - require!( - oo.native_coin_total == 0 - && oo.native_pc_total == 0 - && oo.referrer_rebates_accrued == 0, - MangoError::SomeError - ); - continue; - } // find the TokenInfos for the market's base and quote tokens let base_index = find_token_info_index(&token_infos, serum_account.base_token_index)?; @@ -511,22 +515,19 @@ fn compute_health_detail( base_info.balance = cm!(base_info.balance + base_free * base_info.oracle_price); quote_info.balance = cm!(quote_info.balance + quote_free * quote_info.oracle_price); - // for the amounts that are reserved for orders, compute the worst case for health - // by checking if everything-is-base or everything-is-quote produces worse - // outcomes + // add the reserved amount to both sides, to have the worst-case covered let reserved_base = I80F48::from_num(cm!(oo.native_coin_total - oo.native_coin_free)); let reserved_quote = I80F48::from_num(cm!(oo.native_pc_total - oo.native_pc_free)); let reserved_balance = cm!(reserved_base * base_info.oracle_price + reserved_quote * quote_info.oracle_price); - let all_in_base = cm!(base_info.balance + reserved_balance); - let all_in_quote = cm!(quote_info.balance + reserved_balance); - if pair_health(health_type, base_info, all_in_base, quote_info)? - < pair_health(health_type, quote_info, all_in_quote, base_info)? - { - base_info.balance = all_in_base; - } else { - quote_info.balance = all_in_quote; - } + base_info.serum3_max_reserved = cm!(base_info.serum3_max_reserved + reserved_balance); + quote_info.serum3_max_reserved = cm!(quote_info.serum3_max_reserved + reserved_balance); + + serum3_infos.push(Serum3Info { + reserved: reserved_balance, + base_index, + quote_index, + }); } // health contribution from perp accounts @@ -616,6 +617,7 @@ fn compute_health_detail( Ok(HealthCache { token_infos, + serum3_infos, perp_infos, }) } @@ -888,7 +890,9 @@ mod tests { struct TestHealth1Case { token1: i64, token2: i64, + token3: i64, oo_1_2: (u64, u64), + oo_1_3: (u64, u64), perp1: (i64, i64, i64, i64), expected_health: f64, } @@ -898,6 +902,7 @@ mod tests { 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 bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3); bank1 .data() .change_without_fee( @@ -912,15 +917,30 @@ mod tests { I80F48::from(testcase.token2), ) .unwrap(); + bank3 + .data() + .change_without_fee( + account.tokens.get_mut_or_create(5).unwrap().0, + I80F48::from(testcase.token3), + ) + .unwrap(); let mut oo1 = TestAccount::::new_zeroed(); - let serum3account = account.serum3.create(2).unwrap(); - serum3account.open_orders = oo1.pubkey; - serum3account.base_token_index = 4; - serum3account.quote_token_index = 1; + let serum3account1 = account.serum3.create(2).unwrap(); + serum3account1.open_orders = oo1.pubkey; + serum3account1.base_token_index = 4; + serum3account1.quote_token_index = 1; oo1.data().native_pc_total = testcase.oo_1_2.0; oo1.data().native_coin_total = testcase.oo_1_2.1; + let mut oo2 = TestAccount::::new_zeroed(); + let serum3account2 = account.serum3.create(3).unwrap(); + serum3account2.open_orders = oo2.pubkey; + serum3account2.base_token_index = 5; + serum3account2.quote_token_index = 1; + oo2.data().native_pc_total = testcase.oo_1_3.0; + oo2.data().native_coin_total = testcase.oo_1_3.1; + let mut perp1 = TestAccount::::new_zeroed(); perp1.data().group = group; perp1.data().perp_market_index = 9; @@ -941,10 +961,13 @@ mod tests { let ais = vec![ bank1.as_account_info(), bank2.as_account_info(), + bank3.as_account_info(), oracle1.as_account_info(), oracle2.as_account_info(), + oracle3.as_account_info(), perp1.as_account_info(), oo1.as_account_info(), + oo2.as_account_info(), ]; let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap(); @@ -970,7 +993,7 @@ mod tests { let base_price = 5.0; let base_lots_to_quote = 10.0 * base_price; let testcases = vec![ - TestHealth1Case { + TestHealth1Case { // 0 token1: 100, token2: -10, oo_1_2: (20, 15), @@ -982,8 +1005,9 @@ mod tests { - 10.0 * base_price * 1.5 // for perp (scenario: bids execute) + (3.0 + 7.0) * base_lots_to_quote * 0.8 + (-131.0 - 7.0 * base_lots_to_quote), + ..Default::default() }, - TestHealth1Case { + TestHealth1Case { // 1 token1: -100, token2: 10, oo_1_2: (20, 15), @@ -995,27 +1019,90 @@ mod tests { + (10.0 * base_price + (20.0 + 15.0 * base_price)) * 0.5 // for perp (scenario: asks execute) + (-10.0 - 11.0) * base_lots_to_quote * 1.2 + (-131.0 + 11.0 * base_lots_to_quote), + ..Default::default() }, TestHealth1Case { + // 2 perp1: (-1, 100, 0, 0), expected_health: 0.0, ..Default::default() }, TestHealth1Case { + // 3 perp1: (1, -100, 0, 0), expected_health: -100.0 + 0.8 * 1.0 * base_lots_to_quote, ..Default::default() }, TestHealth1Case { + // 4 perp1: (10, 100, 0, 0), expected_health: 0.0, ..Default::default() }, TestHealth1Case { + // 5 perp1: (30, -100, 0, 0), expected_health: 0.0, ..Default::default() }, + TestHealth1Case { // 6, reserved oo funds + token1: -100, + token2: -10, + token3: -10, + oo_1_2: (1, 1), + oo_1_3: (1, 1), + expected_health: + // tokens + -100.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5 + // oo_1_2 (-> token1) + + (1.0 + 5.0) * 1.2 + // oo_1_3 (-> token1) + + (1.0 + 10.0) * 1.2, + ..Default::default() + }, + TestHealth1Case { // 7, reserved oo funds cross the zero balance level + token1: -14, + token2: -10, + token3: -10, + oo_1_2: (1, 1), + oo_1_3: (1, 1), + expected_health: + // tokens + -14.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5 + // oo_1_2 (-> token1) + + 3.0 * 1.2 + 3.0 * 0.8 + // oo_1_3 (-> token1) + + 8.0 * 1.2 + 3.0 * 0.8, + ..Default::default() + }, + TestHealth1Case { // 8, reserved oo funds in a non-quote currency + token1: -100, + token2: -100, + token3: -1, + oo_1_2: (0, 0), + oo_1_3: (10, 1), + expected_health: + // tokens + -100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5 + // oo_1_3 (-> token3) + + 10.0 * 1.5 + 10.0 * 0.5, + ..Default::default() + }, + TestHealth1Case { // 9, like 8 but oo_1_2 flips the oo_1_3 target + token1: -100, + token2: -100, + token3: -1, + oo_1_2: (100, 0), + oo_1_3: (10, 1), + expected_health: + // tokens + -100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5 + // oo_1_2 (-> token1) + + 80.0 * 1.2 + 20.0 * 0.8 + // oo_1_3 (-> token1) + + 20.0 * 0.8, + ..Default::default() + }, ]; for (i, testcase) in testcases.iter().enumerate() { diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index d25fc4b6c..ab874d735 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -177,7 +177,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> { } // TODO: actual explicit CU comparisons. - // On 2022-5-25 the final deposit costs 52252 CU and each new market increases it by roughly 4400 CU + // On 2022-6-21 the final deposit costs 54074 CU and each new market increases it by roughly 4500 CU Ok(()) }