From 20eef53f9c4a399579582384333ada182ad93f77 Mon Sep 17 00:00:00 2001 From: Lou-Kamades Date: Tue, 16 Apr 2024 11:58:22 -0500 Subject: [PATCH] implement fallbacks for perp_liq_base_or_positive_pnl --- .../mango-v4/src/health/account_retriever.rs | 2 +- .../perp_liq_base_or_positive_pnl.rs | 10 +- .../tests/cases/test_liq_perps_bankruptcy.rs | 3 +- .../cases/test_liq_perps_force_cancel.rs | 6 +- .../cases/test_liq_perps_positive_pnl.rs | 366 ++++++++++++++++++ 5 files changed, 381 insertions(+), 6 deletions(-) diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 811a973cb1..76a9c46e12 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -508,7 +508,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let sol_oracle_index = ais[fallback_oracles_start..] .iter() .position(|o| o.key == &pyth_mainnet_sol_oracle::ID); - + Ok(Self { banks_and_oracles: ScannedBanksAndOracles { banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?, diff --git a/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs index 54d1916561..941a32aa4c 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs @@ -70,8 +70,16 @@ pub fn perp_liq_base_or_positive_pnl( // Get oracle price for market. Price is validated inside let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?; + let fallback_opt = if perp_market.fallback_oracle != Pubkey::default() { + ctx.remaining_accounts + .iter() + .find(|a| a.key == &perp_market.fallback_oracle) + .map(|k| AccountInfoRef::borrow(k).unwrap()) + } else { + None + }; let oracle_price = perp_market.oracle_price( - &OracleAccountInfos::from_reader(oracle_ref), + &OracleAccountInfos::from_reader_with_fallback(oracle_ref, fallback_opt.as_ref()), None, // checked in health )?; diff --git a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs index 48b9c1a5f3..623450e94a 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs @@ -456,7 +456,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { /// Copy of the above test with an added fallback oracle + staleness instructions async fn test_liq_perps_bankruptcy_stale_oracle() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); - test_builder.test().set_compute_max_units(400_000); // PerpLiqNegativePnlOrBankruptcy takes a lot of CU + test_builder.test().set_compute_max_units(200_000); // PerpLiqNegativePnlOrBankruptcy takes a lot of CU let context = test_builder.start_default().await; let solana = &context.solana.clone(); @@ -836,7 +836,6 @@ async fn test_liq_perps_bankruptcy_stale_oracle() -> Result<(), TransportError> is_writable: false, is_signer: false, }; - assert!(send_tx_with_extra_accounts( solana, PerpLiqNegativePnlOrBankruptcyInstruction { diff --git a/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs b/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs index cdf47a47c6..6a6bb43d3e 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs @@ -407,7 +407,7 @@ async fn test_liq_perps_force_cancel_stale_oracle() -> Result<(), TransportError .is_err()); // can withdraw with fallback - send_tx_with_extra_accounts( + assert!(send_tx_with_extra_accounts( solana, TokenWithdrawInstruction { amount: 1, @@ -420,7 +420,9 @@ async fn test_liq_perps_force_cancel_stale_oracle() -> Result<(), TransportError vec![fallback_oracle_meta.clone()], ) .await - .unwrap(); + .unwrap() + .result + .is_ok()); Ok(()) } diff --git a/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs b/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs index cf734f1f24..7fa82ebf03 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs @@ -1,4 +1,5 @@ use super::*; +use anchor_lang::prelude::AccountMeta; #[tokio::test] async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> { @@ -409,3 +410,368 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_liq_perps_positive_pnl_stale_oracle() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(170_000); // PerpLiqBaseOrPositivePnlInstruction takes a lot of CU + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..4]; + let payer_mint_accounts = &context.users[1].token_accounts[0..4]; + + // + // SETUP: Create a group and an account to fill the vaults + // + + let GroupWithTokens { + group, + tokens, + insurance_vault, + .. + } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + zero_token_is_quote: true, + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + // fund the insurance vault + let insurance_vault_funding = 100; + { + let mut tx = ClientTransaction::new(solana); + tx.add_instruction_direct( + spl_token::instruction::transfer( + &spl_token::ID, + &payer_mint_accounts[0], + &insurance_vault, + &payer.pubkey(), + &[&payer.pubkey()], + insurance_vault_funding, + ) + .unwrap(), + ); + tx.add_signer(payer); + tx.send().await.unwrap(); + } + + let _quote_token = &tokens[0]; + let base_token = &tokens[1]; + let borrow_token = &tokens[2]; + let settle_token = &tokens[3]; + + // deposit some funds, to the vaults aren't empty + let liqor = create_funded_account( + &solana, + group, + owner, + 250, + &context.users[1], + mints, + 10000, + 0, + ) + .await; + + // + // SETUP: Create a perp market + // + let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx( + solana, + PerpCreateMarketInstruction { + group, + admin, + payer, + perp_market_index: 0, + settle_token_index: 3, + quote_lot_size: 10, + base_lot_size: 100, + maint_base_asset_weight: 0.8, + init_base_asset_weight: 0.5, + maint_base_liab_weight: 1.2, + init_base_liab_weight: 1.5, + maint_overall_asset_weight: 0.0, + init_overall_asset_weight: 0.0, + base_liquidation_fee: 0.05, + positive_pnl_liquidation_fee: 0.05, + maker_fee: 0.0, + taker_fee: 0.0, + group_insurance_fund: true, + settle_pnl_limit_factor: 0.2, + settle_pnl_limit_window_size_ts: 24 * 60 * 60, + ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await + }, + ) + .await + .unwrap(); + + set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 10.0).await; + let price_lots = { + let perp_market = solana.get_account::(perp_market).await; + perp_market.native_price_to_lot(I80F48::from(10)) + }; + + // + // SETUP: Make an two accounts and deposit some quote and base + // + let context_ref = &context; + let make_account = |idx: u32| async move { + let deposit_amount = 10000; + let account = create_funded_account( + &solana, + group, + owner, + idx, + &context_ref.users[1], + &mints[0..1], + deposit_amount, + 0, + ) + .await; + + account + }; + let account_0 = make_account(0).await; + let account_1 = make_account(1).await; + + // + // SETUP: Borrow some spot on account_0, so we can later make it liquidatable that way + // (actually borrowing 1000.5 due to loan origination!) + // + send_tx( + solana, + TokenWithdrawInstruction { + amount: 1000, + allow_borrow: true, + account: account_0, + owner, + token_account: payer_mint_accounts[2], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // + // SETUP: Trade perps between accounts + // + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 10, + ..PerpPlaceOrderInstruction::default() + }, + ) + .await + .unwrap(); + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 10, + ..PerpPlaceOrderInstruction::default() + }, + ) + .await + .unwrap(); + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + + // after this order exchange it is changed by + // 10*10*100*(0.5-1)*1.4 = -7000 for the long account0 + // 10*10*100*(1-1.5)*1.4 = -7000 for the short account1 + // (100 is base lot size) + assert_eq!( + account_init_health(solana, account_0).await.round(), + (10000.0f64 - 1000.5 * 1.4 - 7000.0).round() + ); + assert_eq!( + account_init_health(solana, account_1).await.round(), + 10000.0 - 7000.0 + ); + + // + // SETUP: Change the perp oracle to make perp-based health go positive for account_0 + // perp base value goes to 10*21*100*0.5, exceeding the negative quote + // perp uhupnl is 10*21*100*0.5 - 10*10*100 = 500 + // but health doesn't exceed 10k because of the 0 overall weight + // + set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 21.0).await; + assert_eq!( + account_init_health(solana, account_0).await.round(), + (10000.0f64 - 1000.5 * 1.4).round() + ); + + // + // SETUP: Increase the price of the borrow so account_0 becomes liquidatable + // + set_bank_stub_oracle_price(solana, group, &borrow_token, admin, 10.0).await; + assert_eq!( + account_init_health(solana, account_0).await.round(), + (10000.0f64 - 10.0 * 1000.5 * 1.4).round() + ); + + // + // SETUP: Fallback oracle + // + let fallback_oracle_kp = TestKeypair::new(); + let fallback_oracle = fallback_oracle_kp.pubkey(); + send_tx( + solana, + StubOracleCreate { + oracle: fallback_oracle_kp, + group, + mint: base_token.mint.pubkey, + admin, + payer, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpAddFallbackOracle { + group, + admin, + perp_market, + fallback_oracle, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + TokenEdit { + group, + admin, + mint: base_token.mint.pubkey, + fallback_oracle, + options: mango_v4::instruction::TokenEdit { + set_fallback_oracle: true, + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + // + // SETUP: Change the oracle to be invalid + // + send_tx( + solana, + StubOracleSetTestInstruction { + oracle: base_token.oracle, + group, + mint: base_token.mint.pubkey, + admin, + price: 21.0, + last_update_slot: 0, + deviation: 100.0, + }, + ) + .await + .unwrap(); + + // + // SETUP: Ensure fallback oracle matches default + // + send_tx( + solana, + StubOracleSetTestInstruction { + oracle: fallback_oracle, + group, + mint: base_token.mint.pubkey, + admin, + price: 21.0, + last_update_slot: 0, + deviation: 0.0, + }, + ) + .await + .unwrap(); + + // + // TEST: PerpLiqBaseOrPositivePnlInstruction fails with stale oracle + // + assert!(send_tx( + solana, + PerpLiqBaseOrPositivePnlInstruction { + liqor, + liqor_owner: owner, + liqee: account_0, + perp_market, + max_base_transfer: i64::MAX, + max_pnl_transfer: 100, + }, + ) + .await + .is_err()); + + // + // TEST: PerpLiqBaseOrPositivePnlInstruction succeeds with fallback + // + let fallback_oracle_meta = AccountMeta { + pubkey: fallback_oracle, + is_writable: false, + is_signer: false, + }; + assert!(send_tx_with_extra_accounts( + solana, + PerpLiqBaseOrPositivePnlInstruction { + liqor, + liqor_owner: owner, + liqee: account_0, + perp_market, + max_base_transfer: i64::MAX, + max_pnl_transfer: 100, + }, + vec![fallback_oracle_meta], + ) + .await + .unwrap() + .result + .is_ok()); + + let liqor_data = solana.get_account::(liqor).await; + assert_eq!(liqor_data.perps[0].base_position_lots(), 0); + assert_eq!(liqor_data.perps[0].quote_position_native(), 100); + assert_eq!( + account_position(solana, liqor, settle_token.bank).await, + 10000 - 95 + ); + let liqee_data = solana.get_account::(account_0).await; + assert_eq!(liqee_data.perps[0].base_position_lots(), 10); + assert_eq!(liqee_data.perps[0].quote_position_native(), -10100); + assert_eq!( + account_position(solana, account_0, settle_token.bank).await, + 95 + ); + + Ok(()) +}