diff --git a/CHANGELOG.md b/CHANGELOG.md index e9cc3fb8aa..1a2c96630c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- program: relax filling conditions for low risk orders vs amm ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) +- sdk: make oracle validity match program and propogate to dlob and math functions ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) + ### Features ### Fixes diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6d4533dd33..67d4643ba6 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -62,7 +62,7 @@ use crate::state::order_params::{ ModifyOrderParams, OrderParams, PlaceOrderOptions, PostOnlyParam, }; use crate::state::paused_operations::{PerpOperation, SpotOperation}; -use crate::state::perp_market::{AMMAvailability, MarketStatus, PerpMarket}; +use crate::state::perp_market::{MarketStatus, PerpMarket}; use crate::state::perp_market_map::PerpMarketMap; use crate::state::protected_maker_mode_config::ProtectedMakerParams; use crate::state::spot_fulfillment_params::{ExternalSpotFill, SpotFulfillmentParams}; @@ -88,10 +88,6 @@ mod tests; #[cfg(test)] mod amm_jit_tests; - -#[cfg(test)] -mod amm_lp_jit_tests; - #[cfg(test)] mod fuel_tests; @@ -998,13 +994,8 @@ pub fn fill_perp_order( .position(|order| order.order_id == order_id && order.status == OrderStatus::Open) .ok_or_else(print_error!(ErrorCode::OrderDoesNotExist))?; - let (order_status, market_index, order_market_type, order_direction) = get_struct_values!( - user.orders[order_index], - status, - market_index, - market_type, - direction - ); + let (order_status, market_index, order_market_type) = + get_struct_values!(user.orders[order_index], status, market_index, market_type); validate!( order_market_type == MarketType::Perp, @@ -1070,14 +1061,9 @@ pub fn fill_perp_order( let safe_oracle_validity: OracleValidity; let oracle_price: i64; let oracle_twap_5min: i64; - let perp_market_index: u16; let user_can_skip_duration: bool; - let amm_can_skip_duration: bool; - let amm_has_low_enough_inventory: bool; - let oracle_valid_for_amm_fill: bool; let oracle_stale_for_margin: bool; - let min_auction_duration: u8; - let mut amm_is_available = !state.amm_paused()?; + let mut amm_is_available: bool = !state.amm_paused()?; { let market = &mut perp_market_map.get_ref_mut(&market_index)?; validation::perp_market::validate_perp_market(market)?; @@ -1104,10 +1090,19 @@ pub fn fill_perp_order( &market.amm.oracle_source, oracle::LogMode::SafeMMOracle, market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; - oracle_valid_for_amm_fill = - is_oracle_valid_for_action(safe_oracle_validity, Some(DriftAction::FillOrderAmm))?; + user_can_skip_duration = user.can_skip_auction_duration(user_stats)?; + amm_is_available &= market.amm_can_fill_order( + &user.orders[order_index], + slot, + fill_mode, + state, + safe_oracle_validity, + user_can_skip_duration, + &mm_oracle_price_data, + )?; oracle_stale_for_margin = mm_oracle_price_data.get_delay() > state @@ -1115,40 +1110,12 @@ pub fn fill_perp_order( .validity .slots_before_stale_for_margin; - amm_is_available &= oracle_valid_for_amm_fill; - amm_is_available &= !market.is_operation_paused(PerpOperation::AmmFill); - amm_is_available &= !market.has_too_much_drawdown()?; - - // We are already using safe oracle data from MM oracle. - // But AMM isnt available if we could have used MM oracle but fell back due to price diff - let amm_available_mm_oracle_recent_but_volatile = - if mm_oracle_price_data.is_enabled() && mm_oracle_price_data.is_mm_oracle_as_recent() { - let amm_available = !mm_oracle_price_data.is_mm_exchange_diff_bps_high(); - amm_available - } else { - true - }; - amm_is_available &= amm_available_mm_oracle_recent_but_volatile; - - let amm_wants_to_jit_make = market.amm.amm_wants_to_jit_make(order_direction)?; - amm_has_low_enough_inventory = market - .amm - .amm_has_low_enough_inventory(amm_wants_to_jit_make)?; - amm_can_skip_duration = - market.can_skip_auction_duration(&state, amm_has_low_enough_inventory)?; - - user_can_skip_duration = user.can_skip_auction_duration(user_stats)?; - reserve_price_before = market.amm.reserve_price()?; oracle_price = mm_oracle_price_data.get_price(); oracle_twap_5min = market .amm .historical_oracle_data .last_oracle_price_twap_5min; - perp_market_index = market.market_index; - - min_auction_duration = - market.get_min_perp_auction_duration(state.min_perp_auction_duration); } // allow oracle price to be used to calculate limit price if it's valid or stale for amm @@ -1156,7 +1123,7 @@ pub fn fill_perp_order( if is_oracle_valid_for_action(safe_oracle_validity, Some(DriftAction::OracleOrderPrice))? { Some(oracle_price) } else { - msg!("Perp market = {} oracle deemed invalid", perp_market_index); + msg!("Perp market = {} oracle deemed invalid", market_index); None }; @@ -1285,16 +1252,6 @@ pub fn fill_perp_order( return Ok((0, 0)); } - let amm_availability = if amm_is_available { - if amm_can_skip_duration && user_can_skip_duration { - AMMAvailability::Immediate - } else { - AMMAvailability::AfterMinDuration - } - } else { - AMMAvailability::Unavailable - }; - let (base_asset_amount, quote_asset_amount) = fulfill_perp_order( user, order_index, @@ -1315,8 +1272,7 @@ pub fn fill_perp_order( valid_oracle_price, now, slot, - min_auction_duration, - amm_availability, + amm_is_available, fill_mode, oracle_stale_for_margin, rev_share_escrow, @@ -1782,8 +1738,7 @@ fn fulfill_perp_order( valid_oracle_price: Option, now: i64, slot: u64, - min_auction_duration: u8, - amm_availability: AMMAvailability, + amm_is_available: bool, fill_mode: FillMode, oracle_stale_for_margin: bool, rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, @@ -1807,19 +1762,13 @@ fn fulfill_perp_order( let fulfillment_methods = { let market = perp_market_map.get_ref(&market_index)?; - let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; - determine_perp_fulfillment_methods( &user.orders[user_order_index], maker_orders_info, &market.amm, reserve_price_before, - Some(oracle_price), limit_price, - amm_availability, - slot, - min_auction_duration, - fill_mode, + amm_is_available, )? }; @@ -3138,6 +3087,7 @@ pub fn trigger_order( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let is_oracle_valid = @@ -5435,6 +5385,7 @@ pub fn trigger_spot_order( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let strict_oracle_price = StrictOraclePrice { current: oracle_price_data.price, diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index c14fd58b62..87c160b78d 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -2,8 +2,14 @@ use anchor_lang::prelude::Pubkey; use anchor_lang::Owner; use crate::math::constants::ONE_BPS_DENOMINATOR; +use crate::math::oracle; +use crate::math::oracle::oracle_validity; +use crate::state::fill_mode::FillMode; use crate::state::oracle_map::OracleMap; +use crate::state::perp_market::PerpMarket; +use crate::state::state::State; use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::user::MarketType; use crate::state::user::{Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -25,6 +31,53 @@ fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { (Pubkey::default(), Pubkey::default(), Pubkey::default()) } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = oracle_validity( + MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + #[cfg(test)] pub mod amm_jit { use std::str::FromStr; @@ -41,6 +94,7 @@ pub mod amm_jit { SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::constants::{CONCENTRATION_PRECISION, PRICE_PRECISION_U64}; + use crate::state::fill_mode::FillMode; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -276,9 +330,21 @@ pub mod amm_jit { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -296,8 +362,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -468,9 +533,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -488,8 +565,7 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -507,7 +583,10 @@ pub mod amm_jit { // nets to zero let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, 0); + assert_eq!( + market_after.amm.base_asset_amount_with_amm, + BASE_PRECISION_I128 / 2 + ); // make sure lps didnt get anything assert_eq!(market_after.amm.base_asset_amount_per_lp, 0); @@ -668,9 +747,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -688,8 +779,7 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -867,9 +957,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -887,8 +989,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1066,11 +1167,24 @@ pub mod amm_jit { }; create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1088,8 +1202,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1278,9 +1391,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1298,8 +1423,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1506,8 +1630,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::Unavailable, + false, FillMode::Fill, false, &mut None, @@ -1688,9 +1811,21 @@ pub mod amm_jit { let reserve_price_before = market.amm.reserve_price().unwrap(); assert_eq!(reserve_price_before, 100 * PRICE_PRECISION_U64); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1708,8 +1843,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1877,10 +2011,22 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + // fulfill with match let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1898,8 +2044,7 @@ pub mod amm_jit { Some(1), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -2079,10 +2224,22 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + // fulfill with match let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2100,8 +2257,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -2332,10 +2488,11 @@ pub mod amm_jit { create_anchor_account_info!(maker, &maker_key, User, maker_account_info); let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + let order_index = 0; // fulfill with match fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2353,8 +2510,7 @@ pub mod amm_jit { Some(1), now, slot, - auction_duration, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + false, FillMode::Fill, false, &mut None, @@ -2619,9 +2775,23 @@ pub mod amm_jit { let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); // fulfill with match + + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = + taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2639,8 +2809,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -2850,9 +3019,22 @@ pub mod amm_jit { assert_eq!(taker.perp_positions[0].open_orders, 1); // fulfill with match + + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2870,8 +3052,7 @@ pub mod amm_jit { Some(1), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs deleted file mode 100644 index ae6666328e..0000000000 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ /dev/null @@ -1,2516 +0,0 @@ -use anchor_lang::prelude::Pubkey; -use anchor_lang::Owner; - -use crate::math::constants::ONE_BPS_DENOMINATOR; -use crate::state::oracle_map::OracleMap; -use crate::state::state::{FeeStructure, FeeTier}; -use crate::state::user::{Order, PerpPosition}; - -fn get_fee_structure() -> FeeStructure { - let mut fee_tiers = [FeeTier::default(); 10]; - fee_tiers[0] = FeeTier { - fee_numerator: 5, - fee_denominator: ONE_BPS_DENOMINATOR, - maker_rebate_numerator: 3, - maker_rebate_denominator: ONE_BPS_DENOMINATOR, - ..FeeTier::default() - }; - FeeStructure { - fee_tiers, - ..FeeStructure::test_default() - } -} - -fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { - (Pubkey::default(), Pubkey::default(), Pubkey::default()) -} - -#[cfg(test)] -pub mod amm_lp_jit { - use std::str::FromStr; - - use crate::controller::orders::fulfill_perp_order; - use crate::controller::position::PositionDirection; - use crate::create_account_info; - use crate::create_anchor_account_info; - use crate::math::constants::{ - PERCENTAGE_PRECISION_I128, PRICE_PRECISION_I64, QUOTE_PRECISION_I64, - }; - - use crate::math::amm_jit::calculate_amm_jit_liquidity; - use crate::math::amm_spread::calculate_inventory_liquidity_ratio; - use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, - PEG_PRECISION, PRICE_PRECISION, SPOT_BALANCE_PRECISION_U64, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, - }; - use crate::math::constants::{CONCENTRATION_PRECISION, PRICE_PRECISION_U64}; - use crate::state::fill_mode::FillMode; - use crate::state::oracle::{HistoricalOracleData, OracleSource}; - use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; - use crate::state::perp_market_map::PerpMarketMap; - use crate::state::spot_market::{SpotBalanceType, SpotMarket}; - use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{OrderStatus, OrderType, SpotPosition, User, UserStats}; - use crate::state::user_map::{UserMap, UserStatsMap}; - use crate::test_utils::*; - use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; - use crate::validation::perp_market::validate_perp_market; - - use super::*; - - #[test] - fn max_jit_amounts() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 2000, - short_spread: 2000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99_920_000, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 300000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.long_spread = 11000; - market.amm.short_spread = 11000; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 45454000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 101 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 45454000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 102 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 104 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.short_spread = 20000; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 104 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 105 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.long_spread = 51000; - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 105 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 9803000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 95 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - } - - #[test] - fn zero_asks_with_amm_lp_jit_taker_long() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.base_asset_reserve = 0; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Short, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - } - - #[test] - fn no_fulfill_with_amm_lp_jit_taker_long() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(21, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (21 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (21 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (21 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Active, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - assert_eq!(new_bid_quote_asset_reserve, 99000000000); - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - price: 100 * PRICE_PRECISION_U64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - - let mut filler_stats = UserStats::default(); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - // amm jit doesnt take anything - assert_eq!( - market_after.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_amm - ); - assert_eq!( - market_after.amm.base_asset_amount_per_lp, - market.amm.base_asset_amount_per_lp - ); - assert_eq!( - market_after.amm.quote_asset_amount_per_lp, - market.amm.quote_asset_amount_per_lp - ); - assert_eq!(market_after.amm.total_fee_minus_distributions, 7500); - assert_eq!(market_after.amm.total_exchange_fee, 7500); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short_max_amount() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let taker_mul: i64 = 20; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 * taker_mul as u64, // if amm takes half it would flip - slot: 0, - price: 100 * PRICE_PRECISION_U64, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 * taker_mul, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64 * taker_mul as u64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 * taker_mul as u64, // maker wants full = amm wants BASE_PERCISION - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 * taker_mul, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64 * taker_mul as u64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - // nets to zero - assert_eq!(market_after.amm.base_asset_amount_with_amm, 0); - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - // maker got (full - net_baa) - assert_eq!( - maker_position.base_asset_amount as i128, - BASE_PRECISION_I128 * taker_mul as i128 - market.amm.base_asset_amount_with_amm - ); - } - - #[test] - fn no_fulfill_with_amm_lp_jit_taker_short() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // amm is short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - // bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - // bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - // ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - // ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - assert_eq!(new_bid_quote_asset_reserve, 99000000000); - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, // doesnt improve balance - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64); - - let taker_position = &taker.perp_positions[0]; - assert_eq!(taker_position.base_asset_amount, -BASE_PRECISION_I64); - assert!(taker.orders[0].is_available()); - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - assert_eq!(maker_position.base_asset_amount, BASE_PRECISION_I64 / 2); - assert_eq!(maker_position.quote_asset_amount, -49985000); - assert_eq!(maker_position.quote_entry_amount, -50 * QUOTE_PRECISION_I64); - assert_eq!(maker_position.quote_break_even_amount, -49985000); - assert_eq!(maker_position.open_orders, 0); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, -1000000000); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - base_spread: 250, - long_spread: 125, - short_spread: 125, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - // base is filled - assert!(base_asset_amount > 0); - - let market_after = market_map.get_ref(&0).unwrap(); - assert!( - market_after.amm.base_asset_amount_with_amm.abs() - < market.amm.base_asset_amount_with_amm.abs() - ); - - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus > 0); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_long() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 99 * PRICE_PRECISION_I64, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - let reserve_price_before = market.amm.reserve_price().unwrap(); - assert_eq!(reserve_price_before, 100 * PRICE_PRECISION_U64); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - // net baa improves - let market_after = market_map.get_ref(&0).unwrap(); - assert!( - market_after.amm.base_asset_amount_with_amm.abs() - < market.amm.base_asset_amount_with_amm.abs() - ); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_long_neg_qas() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 50, // !! amm will bid before the ask spread price - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 10 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 10 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 3 / 4); // auctions not over so no amm fill - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - assert_eq!(maker_position.base_asset_amount, -BASE_PRECISION_I64 / 2); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, -250000000); - - let taker_position = &taker.perp_positions[0]; - assert_eq!( - taker_position.base_asset_amount, - BASE_PRECISION_I64 + market_after.amm.base_asset_amount_with_amm as i64 - ); - - // market pays extra for trade - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus < 0); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short_neg_qas() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 100, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_end_price: 0, - auction_start_price: 200 * PRICE_PRECISION as i64, - auction_duration: 50, // !! amm will bid before the ask spread price - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 200 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 200 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 3 / 4); // auctions not over so no amm fill - - let market_after = market_map.get_ref(&0).unwrap(); - - let taker_position = &taker.perp_positions[0]; - assert_eq!( - taker_position.base_asset_amount, - -3 * BASE_PRECISION_I64 / 4 - ); - - // mm gains from trade - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus < 0); - } - - #[allow(clippy::comparison_chain)] - #[test] - fn fulfill_with_amm_lp_jit_full_long() { - let now = 0_i64; - let mut slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let reserves = 5 * AMM_RESERVE_PRECISION; - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: reserves, - quote_asset_reserve: reserves, - base_asset_amount_with_amm: -(100 * AMM_RESERVE_PRECISION as i128), - base_asset_amount_short: -(100 * AMM_RESERVE_PRECISION as i128), - sqrt_k: reserves, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - base_spread: 5000, - max_spread: 1000000, - long_spread: 50000, - short_spread: 50000, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let auction_duration = 50; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: 100 * BASE_PRECISION_U64, - slot: 0, - auction_duration, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: -100 * BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let auction_start_price = 95062500_i64; - let auction_end_price = 132154089_i64; - taker.orders[0].auction_start_price = auction_start_price; - taker.orders[0].auction_end_price = auction_end_price; - println!("start stop {} {}", auction_start_price, auction_end_price); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (mut neg, mut pos, mut none) = (false, false, false); - let mut prev_mm_fee = 0; - let mut prev_net_baa = market.amm.base_asset_amount_with_amm; - // track scaling - let mut prev_qas = 0; - let mut has_set_prev_qas = false; - loop { - println!("------"); - - // compute auction price - let is_complete = crate::math::auction::is_auction_complete( - taker.orders[0].slot, - auction_duration, - slot, - ) - .unwrap(); - if is_complete { - break; - } - - let auction_price = crate::math::auction::calculate_auction_price( - &taker.orders[0], - slot, - 1, - None, - false, - ) - .unwrap(); - let baa = market.amm.order_step_size * 4; - - let (mark, ask, bid) = { - let market = market_map.get_ref(&0).unwrap(); - let mark = market.amm.reserve_price().unwrap(); - let ask = market.amm.ask_price(mark).unwrap(); - let bid = market.amm.bid_price(mark).unwrap(); - (mark, ask, bid) - }; - println!("mark: {} bid ask: {} {}", mark, bid, ask); - - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: baa, - price: auction_price, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -(baa as i64), - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - // fulfill with match - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, auction_price)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - auction_duration, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - prev_mm_fee; - prev_mm_fee = market_after.amm.total_mm_fee; - - // imbalance decreases - assert!(market_after.amm.base_asset_amount_with_amm.abs() < prev_net_baa.abs()); - prev_net_baa = market_after.amm.base_asset_amount_with_amm; - - println!( - "slot {} auction: {} surplus: {}", - slot, auction_price, quote_asset_amount_surplus - ); - - if !has_set_prev_qas { - prev_qas = quote_asset_amount_surplus; - has_set_prev_qas = true; - } else { - // decreasing (amm paying less / earning more) - assert!(prev_qas < quote_asset_amount_surplus); - prev_qas = quote_asset_amount_surplus; - } - - if quote_asset_amount_surplus < 0 { - neg = true; - assert!(!pos); // neg first - } else if quote_asset_amount_surplus > 0 { - pos = true; - assert!(neg); // neg first - // sometimes skips over == 0 surplus - } else { - none = true; - assert!(neg); - assert!(!pos); - } - slot += 1; - } - // auction should go through both position and negative - assert!(neg); - assert!(pos); - // assert!(none); //todo: skips over this (-1 -> 1) - - println!("{} {} {}", neg, pos, none); - } - - #[allow(clippy::comparison_chain)] - #[test] - fn fulfill_with_amm_lp_jit_full_short() { - let now = 0_i64; - let mut slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let reserves = 5 * AMM_RESERVE_PRECISION; - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: reserves, - quote_asset_reserve: reserves, - base_asset_amount_with_amm: 100 * AMM_RESERVE_PRECISION as i128, - base_asset_amount_long: 100 * AMM_RESERVE_PRECISION as i128, - sqrt_k: reserves, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1, - order_tick_size: 1, - oracle: oracle_price_key, - base_spread: 5000, - max_spread: 1000000, - long_spread: 50000, - short_spread: 50000, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let auction_duration = 50; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: 100 * BASE_PRECISION_U64, - slot: 0, - auction_duration, // !! amm will bid before the ask spread price - auction_end_price: 0, - auction_start_price: 200 * PRICE_PRECISION as i64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -100 * BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let auction_start_price = 105062500; - let auction_end_price = 79550209; - taker.orders[0].auction_start_price = auction_start_price; - taker.orders[0].auction_end_price = auction_end_price; - println!("start stop {} {}", auction_start_price, auction_end_price); - - let mut filler = User::default(); - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (mut neg, mut pos, mut none) = (false, false, false); - let mut prev_mm_fee = 0; - let mut prev_net_baa = market.amm.base_asset_amount_with_amm; - // track scaling - let mut prev_qas = 0; - let mut has_set_prev_qas = false; - - loop { - println!("------"); - - // compute auction price - let is_complete = crate::math::auction::is_auction_complete( - taker.orders[0].slot, - auction_duration, - slot, - ) - .unwrap(); - - if is_complete { - break; - } - - let auction_price = crate::math::auction::calculate_auction_price( - &taker.orders[0], - slot, - 1, - None, - false, - ) - .unwrap(); - let baa = 1000 * 4; - - let (mark, ask, bid) = { - let market = market_map.get_ref(&0).unwrap(); - let mark = market.amm.reserve_price().unwrap(); - let ask = market.amm.ask_price(mark).unwrap(); - let bid = market.amm.bid_price(mark).unwrap(); - (mark, ask, bid) - }; - println!("mark: {} bid ask: {} {}", mark, bid, ask); - - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: baa as u64, - price: auction_price, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: baa as i64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - // fulfill with match - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, auction_price)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - prev_mm_fee; - prev_mm_fee = market_after.amm.total_mm_fee; - - // imbalance decreases or remains the same (damm wont always take on positions) - assert!(market_after.amm.base_asset_amount_with_amm.abs() <= prev_net_baa.abs()); - prev_net_baa = market_after.amm.base_asset_amount_with_amm; - - println!( - "slot {} auction: {} surplus: {}", - slot, auction_price, quote_asset_amount_surplus - ); - - if !has_set_prev_qas { - prev_qas = quote_asset_amount_surplus; - has_set_prev_qas = true; - } else { - // decreasing (amm paying less / earning more) - assert!(prev_qas <= quote_asset_amount_surplus); - prev_qas = quote_asset_amount_surplus; - } - - if quote_asset_amount_surplus < 0 { - neg = true; - assert!(!pos); // neg first - } else if quote_asset_amount_surplus > 0 { - pos = true; - assert!(neg); // neg first - // sometimes skips over == 0 surplus - } else { - none = true; - assert!(neg); - assert!(!pos); - } - slot += 1; - } - // auction should go through both position and negative - assert!(neg); - assert!(pos); - assert!(none); - - println!("{} {} {}", neg, pos, none); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_zero_price_long_imbalance() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: ((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_long: ((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, // expired auction - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - next_order_id: 1, - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 10 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - assert_eq!(taker.perp_positions[0].open_orders, 1); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 10 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(taker.perp_positions[0].open_orders, 0); - assert_eq!(base_asset_amount, 1000000000); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.total_mm_fee, 2033008); // jit occured even tho maker offered full amount - assert_eq!(market_after.amm.total_fee, 2057287); - } -} diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index b4021e6b7b..24959f9e3b 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -5,7 +5,7 @@ use crate::math::constants::ONE_BPS_DENOMINATOR; use crate::math::margin::MarginRequirementType; use crate::state::margin_calculation::{MarginCalculation, MarginContext}; use crate::state::oracle_map::OracleMap; -use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::state::{FeeStructure, FeeTier, State}; use crate::state::user::{Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -27,6 +27,53 @@ fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { (Pubkey::default(), Pubkey::default(), Pubkey::default()) } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &crate::state::perp_market::PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = crate::math::oracle::oracle_validity( + crate::state::user::MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + crate::math::oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + crate::state::fill_mode::FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + #[cfg(test)] pub mod fuel_scoring { use std::str::FromStr; @@ -221,9 +268,21 @@ pub mod fuel_scoring { assert_eq!(maker_stats.fuel_deposits, 0); assert_eq!(taker_stats.fuel_deposits, 0); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (ba, qa) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -241,8 +300,7 @@ pub mod fuel_scoring { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index c8428f01d0..5b97c5d14f 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -1,9 +1,11 @@ use anchor_lang::prelude::Pubkey; use anchor_lang::Owner; +use crate::math::oracle::oracle_validity; +use crate::state::fill_mode::FillMode; use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::MarketStatus; -use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::perp_market::{MarketStatus, PerpMarket}; +use crate::state::state::{FeeStructure, FeeTier, State}; use crate::state::user::{MarketType, Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -29,6 +31,53 @@ fn get_oracle_map<'a>() -> OracleMap<'a> { OracleMap::empty() } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = oracle_validity( + MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + crate::math::oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + pub mod fill_order_protected_maker { use std::str::FromStr; @@ -2179,6 +2228,7 @@ pub mod fulfill_order_with_maker_order { market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier().unwrap(), 0, + 0, ) .unwrap(); @@ -3314,9 +3364,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3338,8 +3400,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -3561,9 +3622,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3584,8 +3657,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -3756,9 +3828,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3776,8 +3860,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -3964,9 +4047,21 @@ pub mod fulfill_order { create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3984,8 +4079,7 @@ pub mod fulfill_order { None, now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4132,9 +4226,21 @@ pub mod fulfill_order { let mut taker_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &UserMap::empty(), @@ -4152,8 +4258,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4332,9 +4437,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let result = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4352,8 +4469,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4521,9 +4637,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let result = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4541,8 +4669,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4663,9 +4790,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4683,8 +4822,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::Immediate, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4832,9 +4970,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4852,8 +5002,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -5423,9 +5572,22 @@ pub mod fulfill_order { let taker_before = taker; let maker_before = maker; + + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market_map.get_ref(&0).unwrap(), + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -5443,8 +5605,7 @@ pub mod fulfill_order { None, now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -5669,9 +5830,21 @@ pub mod fulfill_order { create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market_map.get_ref(&0).unwrap(), + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -5689,8 +5862,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -12881,3 +13053,58 @@ mod update_maker_fills_map { assert_eq!(*map.get(&maker_key).unwrap(), -2 * fill as i64); } } + +mod order_is_low_risk_for_amm { + use super::*; + use crate::state::user::{OrderBitFlag, OrderStatus}; + + fn base_perp_order() -> Order { + Order { + status: OrderStatus::Open, + market_type: MarketType::Perp, + slot: 100, + ..Order::default() + } + } + + #[test] + fn older_than_oracle_delay_returns_true() { + let order = base_perp_order(); + let clock_slot = 110u64; + let mm_oracle_delay = 10i64; + + let is_low = order + .is_low_risk_for_amm(mm_oracle_delay, clock_slot, false) + .unwrap(); + assert!(is_low); + } + + #[test] + fn not_older_than_delay_returns_false() { + let order = base_perp_order(); + let clock_slot = 110u64; + + let mm_oracle_delay = 11i64; + + let is_low = order + .is_low_risk_for_amm(mm_oracle_delay, clock_slot, false) + .unwrap(); + assert!(!is_low); + } + + #[test] + fn liquidation_always_low_risk() { + let order = base_perp_order(); + let is_low = order.is_low_risk_for_amm(0, order.slot, true).unwrap(); + assert!(is_low); + } + + #[test] + fn safe_trigger_order_flag_sets_low_risk() { + let mut order = base_perp_order(); + order.add_bit_flag(OrderBitFlag::SafeTriggerOrder); + + let is_low = order.is_low_risk_for_amm(0, order.slot, false).unwrap(); + assert!(is_low); + } +} diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 11911e0356..5a0d44e15e 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -135,6 +135,7 @@ pub fn settle_pnl( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; if !is_oracle_valid_for_action(oracle_validity, Some(DriftAction::SettlePnl))? diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 89f8305340..0741d5df16 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -6,8 +6,7 @@ use crate::controller::repeg::_update_amm; use crate::math::amm::calculate_market_open_bids_asks; use crate::math::constants::{ - AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, BASE_PRECISION, BASE_PRECISION_I64, - PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + BASE_PRECISION, BASE_PRECISION_I64, PRICE_PRECISION_I64, PRICE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::oracle::OracleValidity; @@ -34,7 +33,6 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::SpotPosition; use crate::test_utils::get_anchor_account_bytes; use crate::test_utils::get_hardcoded_pyth_price; -use crate::QUOTE_PRECISION_I64; use anchor_lang::prelude::{AccountLoader, Clock}; use anchor_lang::Owner; use solana_program::pubkey::Pubkey; @@ -949,6 +947,7 @@ fn amm_negative_ref_price_offset_decay_logic() { assert_eq!(perp_market.amm.last_update_slot, 353317544); perp_market.amm.curve_update_intensity = 200; + perp_market.amm.oracle_slot_delay_override = -1; let max_ref_offset = perp_market.amm.get_max_reference_price_offset().unwrap(); diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index 707ccce35a..ade1c2d90c 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -175,7 +175,8 @@ pub fn _update_amm( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, oracle::LogMode::SafeMMOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; let mut amm_update_cost = 0; @@ -230,7 +231,7 @@ pub fn _update_amm( update_spreads(market, reserve_price_after, Some(clock_slot))?; - if is_oracle_valid_for_action(oracle_validity, Some(DriftAction::FillOrderAmm))? { + if is_oracle_valid_for_action(oracle_validity, Some(DriftAction::FillOrderAmmLowRisk))? { if !amm_not_successfully_updated { market.amm.last_update_slot = clock_slot; } @@ -265,6 +266,7 @@ pub fn update_amm_and_check_validity( &market.amm.oracle_source, LogMode::SafeMMOracle, 0, + 0, )?; validate!( diff --git a/programs/drift/src/controller/repeg/tests.rs b/programs/drift/src/controller/repeg/tests.rs index 92d012dd8e..171351437a 100644 --- a/programs/drift/src/controller/repeg/tests.rs +++ b/programs/drift/src/controller/repeg/tests.rs @@ -114,7 +114,8 @@ pub fn update_amm_test() { market.get_max_confidence_interval_multiplier().unwrap(), &market.amm.oracle_source, LogMode::ExchangeOracle, - 0, + state.oracle_guard_rails.validity.slots_before_stale_for_amm as i8, + state.oracle_guard_rails.validity.slots_before_stale_for_amm as i8, ) .unwrap() == OracleValidity::Valid; @@ -249,6 +250,7 @@ pub fn update_amm_test_bad_oracle() { &market.amm.oracle_source, LogMode::None, 0, + 0, ) .unwrap() == OracleValidity::Valid; diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 6e34edb369..f8e1b625e9 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -412,6 +412,7 @@ pub fn update_spot_market_and_check_validity( &spot_market.oracle_source, LogMode::ExchangeOracle, 0, + 0, )?; validate!( diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 198bcd6377..18cc6d3872 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1071,8 +1071,8 @@ pub fn handle_initialize_perp_market( last_oracle_valid: false, target_base_asset_amount_per_lp: 0, per_lp_base: 0, - oracle_slot_delay_override: 0, - taker_speed_bump_override: 0, + oracle_slot_delay_override: -1, + oracle_low_risk_slot_delay_override: 0, amm_spread_adjustment: 0, mm_oracle_sequence_id: 0, net_unsettled_funding_pnl: 0, @@ -4150,20 +4150,20 @@ pub fn handle_update_perp_market_protected_maker_params( #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] -pub fn handle_update_perp_market_taker_speed_bump_override( +pub fn handle_update_perp_market_oracle_low_risk_slot_delay_override( ctx: Context, - taker_speed_bump_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("perp market {}", perp_market.market_index); msg!( - "perp_market.amm.taker_speed_bump_override: {:?} -> {:?}", - perp_market.amm.taker_speed_bump_override, - taker_speed_bump_override + "perp_market.amm.oracle_low_risk_slot_delay_override: {:?} -> {:?}", + perp_market.amm.oracle_low_risk_slot_delay_override, + oracle_low_risk_slot_delay_override ); - perp_market.amm.taker_speed_bump_override = taker_speed_bump_override; + perp_market.amm.oracle_low_risk_slot_delay_override = oracle_low_risk_slot_delay_override; Ok(()) } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3cb9d8e41a..405af3f895 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1789,6 +1789,7 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, )?; step_size = perp_market.amm.order_step_size; tick_size = perp_market.amm.order_tick_size; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 42c9e9f050..a393cf0670 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1583,11 +1583,14 @@ pub mod drift { ) } - pub fn update_perp_market_taker_speed_bump_override( + pub fn update_perp_market_oracle_low_risk_slot_delay_override( ctx: Context, - taker_speed_bump_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> Result<()> { - handle_update_perp_market_taker_speed_bump_override(ctx, taker_speed_bump_override) + handle_update_perp_market_oracle_low_risk_slot_delay_override( + ctx, + oracle_low_risk_slot_delay_override, + ) } pub fn update_perp_market_amm_spread_adjustment( diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index 88049e246f..53263983c0 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -9,13 +9,10 @@ use crate::state::oracle::OraclePriceData; use crate::state::perp_market::ContractTier; use crate::state::user::{Order, OrderBitFlag, OrderType}; -use crate::state::fill_mode::FillMode; -use crate::state::perp_market::{AMMAvailability, PerpMarket}; +use crate::state::perp_market::PerpMarket; use crate::{OrderParams, MAX_PREDICTION_MARKET_PRICE}; use std::cmp::min; -use super::orders::get_posted_slot_from_clock_slot; - #[cfg(test)] mod tests; @@ -234,42 +231,6 @@ pub fn is_auction_complete(order_slot: u64, auction_duration: u8, slot: u64) -> Ok(slots_elapsed > auction_duration.cast()?) } -pub fn can_fill_with_amm( - amm_availability: AMMAvailability, - valid_oracle_price: Option, - order: &Order, - min_auction_duration: u8, - slot: u64, - fill_mode: FillMode, -) -> DriftResult { - Ok(amm_availability != AMMAvailability::Unavailable - && valid_oracle_price.is_some() - && (amm_availability == AMMAvailability::Immediate - || is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?)) -} - -pub fn is_amm_available_liquidity_source( - order: &Order, - min_auction_duration: u8, - slot: u64, - fill_mode: FillMode, -) -> DriftResult { - if fill_mode.is_liquidation() { - return Ok(true); - } - - if order.is_bit_flag_set(OrderBitFlag::SafeTriggerOrder) { - return Ok(true); - } - - if order.is_signed_msg() { - let clock_slot_tail = get_posted_slot_from_clock_slot(slot); - return Ok(clock_slot_tail.wrapping_sub(order.posted_slot_tail) >= min_auction_duration); - } - - Ok(is_auction_complete(order.slot, min_auction_duration, slot)?) -} - pub fn calculate_auction_params_for_trigger_order( order: &Order, oracle_price_data: &OraclePriceData, diff --git a/programs/drift/src/math/fulfillment.rs b/programs/drift/src/math/fulfillment.rs index c4892bedf8..15103bddcd 100644 --- a/programs/drift/src/math/fulfillment.rs +++ b/programs/drift/src/math/fulfillment.rs @@ -1,12 +1,10 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; -use crate::math::auction::can_fill_with_amm; use crate::math::casting::Cast; use crate::math::matching::do_orders_cross; use crate::math::safe_unwrap::SafeUnwrap; -use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; -use crate::state::perp_market::{AMMAvailability, AMM}; +use crate::state::perp_market::AMM; use crate::state::user::Order; use solana_program::pubkey::Pubkey; @@ -18,38 +16,21 @@ pub fn determine_perp_fulfillment_methods( maker_orders_info: &[(Pubkey, usize, u64)], amm: &AMM, amm_reserve_price: u64, - valid_oracle_price: Option, limit_price: Option, - amm_availability: AMMAvailability, - slot: u64, - min_auction_duration: u8, - fill_mode: FillMode, + amm_is_available: bool, ) -> DriftResult> { if order.post_only { return determine_perp_fulfillment_methods_for_maker( order, amm, amm_reserve_price, - valid_oracle_price, limit_price, - amm_availability, - slot, - min_auction_duration, - fill_mode, + amm_is_available, ); } let mut fulfillment_methods = Vec::with_capacity(8); - let can_fill_with_amm = can_fill_with_amm( - amm_availability, - valid_oracle_price, - order, - min_auction_duration, - slot, - fill_mode, - )?; - let maker_direction = order.direction.opposite(); let mut amm_price = match maker_direction { @@ -67,7 +48,7 @@ pub fn determine_perp_fulfillment_methods( break; } - if can_fill_with_amm { + if amm_is_available { let maker_better_than_amm = match order.direction { PositionDirection::Long => *maker_price <= amm_price, PositionDirection::Short => *maker_price >= amm_price, @@ -90,7 +71,7 @@ pub fn determine_perp_fulfillment_methods( } } - if can_fill_with_amm { + if amm_is_available { let taker_crosses_amm = match limit_price { Some(taker_price) => do_orders_cross(maker_direction, amm_price, taker_price), None => true, @@ -108,25 +89,12 @@ fn determine_perp_fulfillment_methods_for_maker( order: &Order, amm: &AMM, amm_reserve_price: u64, - valid_oracle_price: Option, limit_price: Option, - amm_availability: AMMAvailability, - slot: u64, - min_auction_duration: u8, - fill_mode: FillMode, + amm_is_available: bool, ) -> DriftResult> { let maker_direction = order.direction; - let can_fill_with_amm = can_fill_with_amm( - amm_availability, - valid_oracle_price, - order, - min_auction_duration, - slot, - fill_mode, - )?; - - if !can_fill_with_amm { + if !amm_is_available { return Ok(vec![]); } diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index 23e32cb923..d12afa60b1 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -61,12 +61,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 103 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -122,12 +118,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 99 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -195,12 +187,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 101 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -266,12 +254,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -342,12 +326,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -417,12 +397,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -490,12 +466,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -562,12 +534,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -633,12 +601,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -706,12 +670,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -770,12 +730,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -832,12 +788,8 @@ mod determine_perp_fulfillment_methods { &[], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -894,12 +846,8 @@ mod determine_perp_fulfillment_methods { &[], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 9d227bfe8b..e838fc9960 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -265,6 +265,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let mut skip_token_value = false; @@ -517,6 +518,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( .last_oracle_price_twap, quote_spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let strict_quote_price = StrictOraclePrice::new( @@ -535,6 +537,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let perp_position_custom_margin_ratio = @@ -950,6 +953,7 @@ pub fn calculate_user_equity( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; all_oracles_valid &= is_oracle_valid_for_action(oracle_validity, Some(DriftAction::MarginCalc))?; @@ -980,6 +984,7 @@ pub fn calculate_user_equity( .last_oracle_price_twap, quote_spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; all_oracles_valid &= @@ -995,6 +1000,7 @@ pub fn calculate_user_equity( market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; all_oracles_valid &= diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 78be9ce782..1ac2c0e158 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -24,7 +24,10 @@ pub enum OracleValidity { TooUncertain, StaleForMargin, InsufficientDataPoints, - StaleForAMM, + StaleForAMM { + immediate: bool, + low_risk: bool, + }, #[default] Valid, } @@ -37,7 +40,7 @@ impl OracleValidity { OracleValidity::TooUncertain => ErrorCode::OracleTooUncertain, OracleValidity::StaleForMargin => ErrorCode::OracleStaleForMargin, OracleValidity::InsufficientDataPoints => ErrorCode::OracleInsufficientDataPoints, - OracleValidity::StaleForAMM => ErrorCode::OracleStaleForAMM, + OracleValidity::StaleForAMM { .. } => ErrorCode::OracleStaleForAMM, OracleValidity::Valid => unreachable!(), } } @@ -51,7 +54,18 @@ impl fmt::Display for OracleValidity { OracleValidity::TooUncertain => write!(f, "TooUncertain"), OracleValidity::StaleForMargin => write!(f, "StaleForMargin"), OracleValidity::InsufficientDataPoints => write!(f, "InsufficientDataPoints"), - OracleValidity::StaleForAMM => write!(f, "StaleForAMM"), + OracleValidity::StaleForAMM { + immediate, + low_risk, + } => { + if *immediate { + write!(f, "StaleForAMM (immediate)") + } else if *low_risk { + write!(f, "StaleForAMM (low risk)") + } else { + write!(f, "StaleForAMM") + } + } OracleValidity::Valid => write!(f, "Valid"), } } @@ -63,7 +77,8 @@ pub enum DriftAction { SettlePnl, TriggerOrder, FillOrderMatch, - FillOrderAmm, + FillOrderAmmLowRisk, + FillOrderAmmImmediate, Liquidate, MarginCalc, UpdateTwap, @@ -78,15 +93,25 @@ pub fn is_oracle_valid_for_action( ) -> DriftResult { let is_ok = match action { Some(action) => match action { - DriftAction::FillOrderAmm => { + DriftAction::FillOrderAmmImmediate => { matches!(oracle_validity, OracleValidity::Valid) } + DriftAction::FillOrderAmmLowRisk => { + matches!( + oracle_validity, + OracleValidity::Valid + | OracleValidity::StaleForAMM { + immediate: _, + low_risk: false + } + ) + } // relax oracle staleness, later checks for sufficiently recent amm slot update for funding update DriftAction::UpdateFunding => { matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ) @@ -95,7 +120,7 @@ pub fn is_oracle_valid_for_action( matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints ) } @@ -113,7 +138,7 @@ pub fn is_oracle_valid_for_action( DriftAction::SettlePnl => matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ), @@ -184,6 +209,7 @@ pub fn get_oracle_status( guard_rails: &OracleGuardRails, reserve_price: u64, ) -> DriftResult { + let slot_delay_override = guard_rails.validity.slots_before_stale_for_amm.cast()?; let oracle_validity = oracle_validity( MarketType::Perp, market.market_index, @@ -193,7 +219,8 @@ pub fn get_oracle_status( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, LogMode::None, - 0, + slot_delay_override, + slot_delay_override, )?; let oracle_reserve_price_spread_pct = amm::calculate_oracle_twap_5min_price_spread_pct(&market.amm, reserve_price)?; @@ -227,7 +254,8 @@ pub fn oracle_validity( max_confidence_interval_multiplier: u64, oracle_source: &OracleSource, log_mode: LogMode, - slots_before_stale_for_amm_override: i8, + slots_before_stale_for_amm_immdiate_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> DriftResult { let OraclePriceData { price: oracle_price, @@ -252,15 +280,21 @@ pub fn oracle_validity( .confidence_interval_max_size .safe_mul(max_confidence_interval_multiplier)?); - let is_stale_for_amm = if slots_before_stale_for_amm_override != 0 { - oracle_delay.gt(&slots_before_stale_for_amm_override.max(0).cast()?) + let is_stale_for_amm_immediate = if slots_before_stale_for_amm_immdiate_override != 0 { + oracle_delay.gt(&slots_before_stale_for_amm_immdiate_override.max(0).cast()?) + } else { + true + }; + + let is_stale_for_amm_low_risk = if oracle_low_risk_slot_delay_override != 0 { + oracle_delay.gt(&oracle_low_risk_slot_delay_override.max(0).cast()?) } else { oracle_delay.gt(&valid_oracle_guard_rails.slots_before_stale_for_amm) }; let is_stale_for_margin = if matches!( oracle_source, - OracleSource::PythStableCoinPull | OracleSource::PythStableCoin + OracleSource::PythStableCoinPull | OracleSource::PythLazerStableCoin ) { oracle_delay.gt(&(valid_oracle_guard_rails .slots_before_stale_for_margin @@ -279,8 +313,11 @@ pub fn oracle_validity( OracleValidity::StaleForMargin } else if !has_sufficient_number_of_data_points { OracleValidity::InsufficientDataPoints - } else if is_stale_for_amm { - OracleValidity::StaleForAMM + } else if is_stale_for_amm_immediate || is_stale_for_amm_low_risk { + OracleValidity::StaleForAMM { + immediate: is_stale_for_amm_immediate, + low_risk: is_stale_for_amm_low_risk, + } } else { OracleValidity::Valid }; @@ -332,13 +369,16 @@ pub fn oracle_validity( ); } - if is_stale_for_amm || is_stale_for_margin { + if is_stale_for_amm_immediate || is_stale_for_margin || is_stale_for_amm_low_risk { crate::msg!( - "Invalid {} {} {} Oracle: Stale (oracle_delay={:?})", + "Invalid {} {} {} Oracle: Stale (oracle_delay={:?}), (stale_for_amm_immediate={}, stale_for_amm_low_risk={}, stale_for_margin={})", market_type, market_index, oracle_type, - oracle_delay + oracle_delay, + is_stale_for_amm_immediate, + is_stale_for_amm_low_risk, + is_stale_for_margin ); } } diff --git a/programs/drift/src/math/repeg.rs b/programs/drift/src/math/repeg.rs index 4cab184297..995e425f64 100644 --- a/programs/drift/src/math/repeg.rs +++ b/programs/drift/src/math/repeg.rs @@ -46,7 +46,8 @@ pub fn calculate_repeg_validity_from_oracle_account( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, oracle::LogMode::ExchangeOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )? == OracleValidity::Valid; let (oracle_is_valid, direction_valid, profitability_valid, price_impact_valid) = diff --git a/programs/drift/src/math/repeg/tests.rs b/programs/drift/src/math/repeg/tests.rs index 4beb1c7bfb..0619d0b35f 100644 --- a/programs/drift/src/math/repeg/tests.rs +++ b/programs/drift/src/math/repeg/tests.rs @@ -256,7 +256,7 @@ fn calculate_optimal_peg_and_budget_2_test() { let oracle_price_data = OraclePriceData { price: (17_800 * PRICE_PRECISION) as i64, confidence: 10233, - delay: 2, + delay: 0, has_sufficient_number_of_data_points: true, sequence_id: None, }; diff --git a/programs/drift/src/state/oracle_map.rs b/programs/drift/src/state/oracle_map.rs index dc574b8d98..47c0daba1d 100644 --- a/programs/drift/src/state/oracle_map.rs +++ b/programs/drift/src/state/oracle_map.rs @@ -89,6 +89,7 @@ impl<'a> OracleMap<'a> { last_oracle_price_twap: i64, max_confidence_interval_multiplier: u64, slots_before_stale_for_amm_override: i8, + oracle_low_risk_slot_delay_override_override: i8, ) -> DriftResult<(&OraclePriceData, OracleValidity)> { if self.should_get_quote_asset_price_data(&oracle_id.0) { return Ok((&self.quote_asset_price_data, OracleValidity::Valid)); @@ -110,6 +111,7 @@ impl<'a> OracleMap<'a> { &oracle_id.1, LogMode::ExchangeOracle, slots_before_stale_for_amm_override, + oracle_low_risk_slot_delay_override_override, )?; self.validity.insert(*oracle_id, oracle_validity); oracle_validity @@ -140,6 +142,7 @@ impl<'a> OracleMap<'a> { &oracle_id.1, LogMode::ExchangeOracle, slots_before_stale_for_amm_override, + oracle_low_risk_slot_delay_override_override, )?; self.validity.insert(*oracle_id, oracle_validity); diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 45e7cf11ce..f725599f35 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,6 +1,7 @@ use crate::msg; +use crate::state::fill_mode::FillMode; use crate::state::pyth_lazer_oracle::PythLazerOracle; -use crate::state::user::MarketType; +use crate::state::user::{MarketType, Order}; use anchor_lang::prelude::*; use crate::state::state::{State, ValidityGuardRails}; @@ -45,7 +46,9 @@ use static_assertions::const_assert_eq; use super::oracle_map::OracleIdentifier; use super::protected_maker_mode_config::ProtectedMakerParams; -use crate::math::oracle::{oracle_validity, LogMode, OracleValidity}; +use crate::math::oracle::{ + is_oracle_valid_for_action, oracle_validity, DriftAction, LogMode, OracleValidity, +}; #[cfg(test)] mod tests; @@ -136,13 +139,6 @@ impl ContractTier { } } -#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, PartialOrd, Ord)] -pub enum AMMAvailability { - Immediate, - AfterMinDuration, - Unavailable, -} - #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] @@ -328,16 +324,12 @@ impl PerpMarket { let amm_low_inventory_and_profitable = self.amm.net_revenue_since_last_funding >= DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT && amm_has_low_enough_inventory; - let amm_oracle_no_latency = self.amm.oracle_source == OracleSource::Prelaunch - || (self.amm.historical_oracle_data.last_oracle_delay == 0 - && self.amm.oracle_source == OracleSource::PythLazer); - let can_skip = amm_low_inventory_and_profitable || amm_oracle_no_latency; - if can_skip { + if amm_low_inventory_and_profitable { msg!("market {} amm skipping auction duration", self.market_index); } - Ok(can_skip) + Ok(amm_low_inventory_and_profitable) } pub fn has_too_much_drawdown(&self) -> DriftResult { @@ -705,14 +697,6 @@ impl PerpMarket { } } - pub fn get_min_perp_auction_duration(&self, default_min_auction_duration: u8) -> u8 { - if self.amm.taker_speed_bump_override != 0 { - self.amm.taker_speed_bump_override.max(0).unsigned_abs() - } else { - default_min_auction_duration - } - } - pub fn get_trigger_price( &self, oracle_price: i64, @@ -851,6 +835,7 @@ impl PerpMarket { &self.amm.oracle_source, LogMode::MMOracle, self.amm.oracle_slot_delay_override, + self.amm.oracle_low_risk_slot_delay_override, )? }; Ok(MMOraclePriceData::new( @@ -861,6 +846,83 @@ impl PerpMarket { oracle_price_data, )?) } + + pub fn amm_can_fill_order( + &self, + order: &Order, + clock_slot: u64, + fill_mode: FillMode, + state: &State, + safe_oracle_validity: OracleValidity, + user_can_skip_auction_duration: bool, + mm_oracle_price_data: &MMOraclePriceData, + ) -> DriftResult { + if self.is_operation_paused(PerpOperation::AmmFill) { + return Ok(false); + } + + if self.has_too_much_drawdown()? { + return Ok(false); + } + + // We are already using safe oracle data from MM oracle. + // But AMM isnt available if we could have used MM oracle but fell back due to price diff + // This is basically early volatility protection + let mm_oracle_not_too_volatile = + if mm_oracle_price_data.is_enabled() && mm_oracle_price_data.is_mm_oracle_as_recent() { + let amm_available = !mm_oracle_price_data.is_mm_exchange_diff_bps_high(); + amm_available + } else { + true + }; + + if !mm_oracle_not_too_volatile { + msg!("AMM cannot fill order: MM oracle too volatile compared to exchange oracle"); + return Ok(false); + } + + // Determine if order is fillable with low risk + let oracle_valid_for_amm_fill_low_risk = is_oracle_valid_for_action( + safe_oracle_validity, + Some(DriftAction::FillOrderAmmLowRisk), + )?; + if !oracle_valid_for_amm_fill_low_risk { + msg!("AMM cannot fill order: oracle not valid for low risk fills"); + return Ok(false); + } + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let can_fill_low_risk = order.is_low_risk_for_amm( + safe_oracle_price_data.delay, + clock_slot, + fill_mode.is_liquidation(), + )?; + + // Proceed if order is low risk and we can fill it. Otherwise check if we can higher risk order immediately + let can_fill_order = if can_fill_low_risk { + true + } else { + let oracle_valid_for_can_fill_immediately = is_oracle_valid_for_action( + safe_oracle_validity, + Some(DriftAction::FillOrderAmmImmediate), + )?; + if !oracle_valid_for_can_fill_immediately { + msg!("AMM cannot fill order: oracle not valid for immediate fills"); + return Ok(false); + } + let amm_wants_to_jit_make = self.amm.amm_wants_to_jit_make(order.direction)?; + let amm_has_low_enough_inventory = self + .amm + .amm_has_low_enough_inventory(amm_wants_to_jit_make)?; + let amm_can_skip_duration = + self.can_skip_auction_duration(&state, amm_has_low_enough_inventory)?; + + amm_can_skip_duration + && oracle_valid_for_can_fill_immediately + && user_can_skip_auction_duration + }; + + Ok(can_fill_order) + } } #[cfg(test)] @@ -1160,7 +1222,7 @@ pub struct AMM { pub per_lp_base: i8, /// the override for the state.min_perp_auction_duration /// 0 is no override, -1 is disable speed bump, 1-100 is literal speed bump - pub taker_speed_bump_override: i8, + pub oracle_low_risk_slot_delay_override: i8, /// signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double) pub amm_spread_adjustment: i8, pub oracle_slot_delay_override: i8, @@ -1254,9 +1316,9 @@ impl Default for AMM { last_oracle_valid: false, target_base_asset_amount_per_lp: 0, per_lp_base: 0, - taker_speed_bump_override: 0, + oracle_low_risk_slot_delay_override: 0, amm_spread_adjustment: 0, - oracle_slot_delay_override: 0, + oracle_slot_delay_override: -1, mm_oracle_sequence_id: 0, net_unsettled_funding_pnl: 0, quote_asset_amount_with_unsettled_lp: 0, diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index 46de2248b1..82019c047f 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -102,61 +102,6 @@ mod get_margin_ratio { } } -mod get_min_perp_auction_duration { - use crate::state::perp_market::{PerpMarket, AMM}; - use crate::State; - - #[test] - fn test_get_speed_bump() { - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: 0, - ..AMM::default() - }, - ..PerpMarket::default() - }; - - let state = State { - min_perp_auction_duration: 10, - ..State::default() - }; - - // no override uses state value - assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 10 - ); - - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: -1, - ..AMM::default() - }, - ..PerpMarket::default() - }; - - // -1 override disables speed bump - assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 0 - ); - - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: 20, - ..AMM::default() - }, - ..PerpMarket::default() - }; - - // positive override uses override value - assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 20 - ); - } -} - mod get_trigger_price { use crate::state::perp_market::HistoricalOracleData; use crate::state::perp_market::{PerpMarket, AMM}; @@ -336,3 +281,243 @@ mod get_trigger_price { assert_eq!(clamped_price, large_oracle_price + max_oracle_diff_large); } } + +mod amm_can_fill_order_tests { + use crate::controller::position::PositionDirection; + use crate::math::oracle::OracleValidity; + use crate::state::fill_mode::FillMode; + use crate::state::oracle::{MMOraclePriceData, OraclePriceData}; + use crate::state::paused_operations::PerpOperation; + use crate::state::perp_market::{PerpMarket, AMM}; + use crate::state::state::{State, ValidityGuardRails}; + use crate::state::user::{Order, OrderStatus}; + use crate::PRICE_PRECISION_I64; + + fn base_state() -> State { + State { + min_perp_auction_duration: 10, + ..State::default() + } + } + + fn base_market() -> PerpMarket { + PerpMarket { + amm: AMM { + mm_oracle_price: PRICE_PRECISION_I64, + mm_oracle_slot: 0, + order_step_size: 1, + amm_jit_intensity: 100, + ..AMM::default() + }, + ..PerpMarket::default() + } + } + + fn base_order() -> Order { + Order { + status: OrderStatus::Open, + slot: 0, + base_asset_amount: 1, + direction: PositionDirection::Long, + ..Order::default() + } + } + + fn mm_oracle_ok_and_as_recent() -> (MMOraclePriceData, ValidityGuardRails) { + let exchange = OraclePriceData { + price: PRICE_PRECISION_I64, + confidence: 1, + delay: 5, + has_sufficient_number_of_data_points: true, + sequence_id: Some(100), + }; + let mm = + MMOraclePriceData::new(PRICE_PRECISION_I64, 5, 100, OracleValidity::Valid, exchange) + .unwrap(); + (mm, ValidityGuardRails::default()) + } + + #[test] + fn paused_operation_returns_false() { + let mut market = base_market(); + // Pause AMM fill + market.paused_operations = PerpOperation::AmmFill as u8; + let order = base_order(); + let state = base_state(); + let (mm, guard) = mm_oracle_ok_and_as_recent(); + + let can = market + .amm_can_fill_order( + &order, + 10, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } + + #[test] + fn mm_oracle_too_volatile_blocks() { + let market = base_market(); + let order = base_order(); + let state = base_state(); + + // Create MM oracle data with >1% diff vs exchange to force fallback and block + let exchange = OraclePriceData { + price: PRICE_PRECISION_I64, // 1.0 + confidence: 1, + delay: 1, + has_sufficient_number_of_data_points: true, + sequence_id: Some(100), + }; + // 3% higher than exchange + let mm = MMOraclePriceData::new( + PRICE_PRECISION_I64 + (PRICE_PRECISION_I64 / 33), + 1, + 99, + OracleValidity::Valid, + exchange, + ) + .unwrap(); + + let can = market + .amm_can_fill_order( + &order, + 10, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } + + #[test] + fn low_risk_path_succeeds_when_auction_elapsed() { + let market = base_market(); + let mut order = base_order(); + order.slot = 0; + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // clock_slot sufficiently beyond min_auction_duration + let can = market + .amm_can_fill_order( + &order, + 15, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(can); + } + + #[test] + fn low_risk_path_succeeds_when_auction_elapsed_with_stale_for_immediate() { + let market = base_market(); + let mut order = base_order(); + order.slot = 0; // order placed at slot 0 + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // clock_slot sufficiently beyond min_auction_duration + let can = market + .amm_can_fill_order( + &order, + 15, + FillMode::Fill, + &state, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + }, + true, + &mm, + ) + .unwrap(); + assert!(can); + } + + #[test] + fn high_risk_immediate_requires_user_and_market_skip() { + let mut market = base_market(); + let mut order = base_order(); + order.slot = 20; + market.amm.amm_jit_intensity = 100; + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // cnat fill if user cant skip auction duration + let can1 = market + .amm_can_fill_order( + &order, + 21, + FillMode::Fill, + &state, + OracleValidity::Valid, + false, + &mm, + ) + .unwrap(); + assert!(!can1); + + // valid oracle for immediate and user can skip, market can skip due to low inventory => can fill + market.amm.base_asset_amount_with_amm = -2; // taker long improves balance + market.amm.order_step_size = 1; + market.amm.base_asset_reserve = 1_000_000; + market.amm.quote_asset_reserve = 1_000_000; + market.amm.sqrt_k = 1_000_000; + market.amm.max_base_asset_reserve = 2_000_000; + market.amm.min_base_asset_reserve = 0; + + let can2 = market + .amm_can_fill_order( + &order, + 21, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(can2); + } + + #[test] + fn invalid_safe_oracle_validity_blocks_low_risk() { + let market = base_market(); + let order = base_order(); + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // Order is old but invalid oracle validity + let can = market + .amm_can_fill_order( + &order, + 20, + FillMode::Fill, + &state, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + }, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } +} diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b36607e3dc..ef702e90e1 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1524,6 +1524,26 @@ impl Order { || (self.triggered() && !(self.reduce_only && self.is_bit_flag_set(OrderBitFlag::NewTriggerReduceOnly))) } + + pub fn is_low_risk_for_amm( + &self, + mm_oracle_delay: i64, + clock_slot: u64, + is_liquidation: bool, + ) -> DriftResult { + if self.market_type == MarketType::Spot { + return Ok(false); + } + + let order_older_than_oracle_delay = { + let clock_minus_delay = clock_slot.cast::()?.safe_sub(mm_oracle_delay)?; + clock_minus_delay >= self.slot.cast::()? + }; + + Ok(order_older_than_oracle_delay + || is_liquidation + || self.is_bit_flag_set(OrderBitFlag::SafeTriggerOrder)) + } } impl Default for Order { diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index ad5307ad70..693637f55f 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4048,34 +4048,34 @@ export class AdminClient extends DriftClient { ); } - public async updatePerpMarketTakerSpeedBumpOverride( + public async updatePerpMarketOracleLowRiskSlotDelayOverride( perpMarketIndex: number, - takerSpeedBumpOverride: number + oracleLowRiskSlotDelayOverride: number ): Promise { - const updatePerpMarketTakerSpeedBumpOverrideIx = - await this.getUpdatePerpMarketTakerSpeedBumpOverrideIx( + const updatePerpMarketOracleLowRiskSlotDelayOverrideIx = + await this.getUpdatePerpMarketOracleLowRiskSlotDelayOverrideIx( perpMarketIndex, - takerSpeedBumpOverride + oracleLowRiskSlotDelayOverride ); const tx = await this.buildTransaction( - updatePerpMarketTakerSpeedBumpOverrideIx + updatePerpMarketOracleLowRiskSlotDelayOverrideIx ); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdatePerpMarketTakerSpeedBumpOverrideIx( + public async getUpdatePerpMarketOracleLowRiskSlotDelayOverrideIx( perpMarketIndex: number, - takerSpeedBumpOverride: number + oracleLowRiskSlotDelayOverride: number ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, perpMarketIndex ); - return await this.program.instruction.updatePerpMarketTakerSpeedBumpOverride( - takerSpeedBumpOverride, + return await this.program.instruction.updatePerpMarketOracleLowRiskSlotDelayOverride( + oracleLowRiskSlotDelayOverride, { accounts: { admin: this.useHotWalletAdmin diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 15c2ec72f2..5b8ec0c896 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -467,10 +467,6 @@ export class DLOB { const isAmmPaused = ammPaused(stateAccount, marketAccount); - const minAuctionDuration = isVariant(marketType, 'perp') - ? stateAccount.minPerpAuctionDuration - : 0; - const { makerRebateNumerator, makerRebateDenominator } = this.getMakerRebate(marketType, stateAccount, marketAccount); @@ -481,7 +477,8 @@ export class DLOB { marketType, oraclePriceData, isAmmPaused, - minAuctionDuration, + stateAccount, + marketAccount, fallbackAsk, fallbackBid ); @@ -493,7 +490,8 @@ export class DLOB { marketType, oraclePriceData, isAmmPaused, - minAuctionDuration, + stateAccount, + marketAccount, makerRebateNumerator, makerRebateDenominator, fallbackAsk, @@ -597,7 +595,10 @@ export class DLOB { ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, - minAuctionDuration: number, + stateAccount: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount, makerRebateNumerator: number, makerRebateDenominator: number, fallbackAsk: BN | undefined, @@ -636,7 +637,8 @@ export class DLOB { (askPrice) => { return askPrice.lte(fallbackBidWithBuffer); }, - minAuctionDuration + stateAccount, + marketAccount ); for (const askCrossingFallback of asksCrossingFallback) { @@ -664,7 +666,8 @@ export class DLOB { (bidPrice) => { return bidPrice.gte(fallbackAskWithBuffer); }, - minAuctionDuration + stateAccount, + marketAccount ); for (const bidCrossingFallback of bidsCrossingFallback) { @@ -683,7 +686,10 @@ export class DLOB { ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, - minAuctionDuration: number, + state: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount, fallbackAsk: BN | undefined, fallbackBid?: BN | undefined ): NodeToFill[] { @@ -736,7 +742,8 @@ export class DLOB { (takerPrice) => { return takerPrice === undefined || takerPrice.lte(fallbackBid); }, - minAuctionDuration + state, + marketAccount ); for (const takingAskCrossingFallback of takingAsksCrossingFallback) { @@ -793,7 +800,8 @@ export class DLOB { (takerPrice) => { return takerPrice === undefined || takerPrice.gte(fallbackAsk); }, - minAuctionDuration + state, + marketAccount ); for (const marketBidCrossingFallback of takingBidsCrossingFallback) { nodesToFill.push(marketBidCrossingFallback); @@ -911,7 +919,10 @@ export class DLOB { : MMOraclePriceData, nodeGenerator: Generator, doesCross: (nodePrice: BN | undefined) => boolean, - minAuctionDuration: number + state: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount ): NodeToFill[] { const nodesToFill = new Array(); @@ -934,8 +945,10 @@ export class DLOB { isVariant(marketType, 'spot') || isFallbackAvailableLiquiditySource( node.order, - minAuctionDuration, - slot + oraclePriceData as MMOraclePriceData, + slot, + state, + marketAccount as PerpMarketAccount ); if (crosses && fallbackAvailable) { diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index eed45293ca..7f5ca59818 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -6566,7 +6566,7 @@ ] }, { - "name": "updatePerpMarketTakerSpeedBumpOverride", + "name": "updatePerpMarketOracleLowRiskSlotDelayOverride", "accounts": [ { "name": "admin", @@ -6586,7 +6586,7 @@ ], "args": [ { - "name": "takerSpeedBumpOverride", + "name": "oracleLowRiskSlotDelayOverride", "type": "i8" } ] @@ -11194,7 +11194,7 @@ "type": "i8" }, { - "name": "takerSpeedBumpOverride", + "name": "oracleLowRiskSlotDelayOverride", "docs": [ "the override for the state.min_perp_auction_duration", "0 is no override, -1 is disable speed bump, 1-100 is literal speed bump" @@ -12278,7 +12278,17 @@ "name": "InsufficientDataPoints" }, { - "name": "StaleForAMM" + "name": "StaleForAMM", + "fields": [ + { + "name": "immediate", + "type": "bool" + }, + { + "name": "lowRisk", + "type": "bool" + } + ] }, { "name": "Valid" @@ -12304,7 +12314,10 @@ "name": "FillOrderMatch" }, { - "name": "FillOrderAmm" + "name": "FillOrderAmmLowRisk" + }, + { + "name": "FillOrderAmmImmediate" }, { "name": "Liquidate" @@ -12954,23 +12967,6 @@ ] } }, - { - "name": "AMMAvailability", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Immediate" - }, - { - "name": "AfterMinDuration" - }, - { - "name": "Unavailable" - } - ] - } - }, { "name": "RevenueShareOrderBitFlag", "type": { diff --git a/sdk/src/math/auction.ts b/sdk/src/math/auction.ts index cabf5f4860..013f1388b7 100644 --- a/sdk/src/math/auction.ts +++ b/sdk/src/math/auction.ts @@ -1,4 +1,12 @@ -import { isOneOfVariant, isVariant, Order, PositionDirection } from '../types'; +import { + isOneOfVariant, + isVariant, + OracleValidity, + Order, + PerpOperation, + PositionDirection, + StateAccount, +} from '../types'; import { BN } from '@coral-xyz/anchor'; import { ONE, @@ -8,6 +16,10 @@ import { } from '../constants/numericConstants'; import { getVariant, OrderBitFlag, PerpMarketAccount } from '../types'; import { getPerpMarketTierNumber } from './tiers'; +import { MMOraclePriceData } from '../oracles/types'; +import { isLowRiskForAmm } from './orders'; +import { getOracleValidity } from './oracles'; +import { isOperationPaused } from './exchangeStatus'; export function isAuctionComplete(order: Order, slot: number): boolean { if (order.auctionDuration === 0) { @@ -19,18 +31,49 @@ export function isAuctionComplete(order: Order, slot: number): boolean { export function isFallbackAvailableLiquiditySource( order: Order, - minAuctionDuration: number, - slot: number + mmOraclePriceData: MMOraclePriceData, + slot: number, + state: StateAccount, + market: PerpMarketAccount, + isLiquidation?: boolean ): boolean { - if (minAuctionDuration === 0) { - return true; + if (isOperationPaused(market.pausedOperations, PerpOperation.AMM_FILL)) { + return false; + } + + // TODO: include too much drawdown check & mm oracle volatility + + const oracleValidity = getOracleValidity( + market!, + { + price: mmOraclePriceData.price, + slot: mmOraclePriceData.slot, + confidence: mmOraclePriceData.confidence, + hasSufficientNumberOfDataPoints: + mmOraclePriceData.hasSufficientNumberOfDataPoints, + }, + state.oracleGuardRails, + new BN(slot) + ); + if (oracleValidity <= OracleValidity.StaleForAMMLowRisk) { + return false; } - if ((order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0) { + if (oracleValidity == OracleValidity.Valid) { return true; } - return new BN(slot).sub(order.slot).gt(new BN(minAuctionDuration)); + const isOrderLowRiskForAmm = isLowRiskForAmm( + order, + mmOraclePriceData, + isLiquidation + ); + + if (!isOrderLowRiskForAmm) { + return false; + } else { + return true; + } } /** diff --git a/sdk/src/math/oracles.ts b/sdk/src/math/oracles.ts index 6575ed7a1d..27248326f0 100644 --- a/sdk/src/math/oracles.ts +++ b/sdk/src/math/oracles.ts @@ -3,7 +3,9 @@ import { HistoricalOracleData, OracleGuardRails, OracleSource, + OracleValidity, PerpMarketAccount, + isOneOfVariant, isVariant, } from '../types'; import { OraclePriceData } from '../oracles/types'; @@ -14,6 +16,7 @@ import { ZERO, FIVE_MINUTE, PERCENTAGE_PRECISION, + FIVE, } from '../constants/numericConstants'; import { assert } from '../assert/assert'; import { BN } from '@coral-xyz/anchor'; @@ -51,6 +54,91 @@ export function getMaxConfidenceIntervalMultiplier( return maxConfidenceIntervalMultiplier; } +export function getOracleValidity( + market: PerpMarketAccount, + oraclePriceData: OraclePriceData, + oracleGuardRails: OracleGuardRails, + slot: BN, + oracleStalenessBuffer = FIVE +): OracleValidity { + const isNonPositive = oraclePriceData.price.lte(ZERO); + const isTooVolatile = BN.max( + oraclePriceData.price, + market.amm.historicalOracleData.lastOraclePriceTwap + ) + .div( + BN.max( + ONE, + BN.min( + oraclePriceData.price, + market.amm.historicalOracleData.lastOraclePriceTwap + ) + ) + ) + .gt(oracleGuardRails.validity.tooVolatileRatio); + + const confPctOfPrice = oraclePriceData.confidence + .mul(BID_ASK_SPREAD_PRECISION) + .div(oraclePriceData.price); + const isConfTooLarge = confPctOfPrice.gt( + oracleGuardRails.validity.confidenceIntervalMaxSize.mul( + getMaxConfidenceIntervalMultiplier(market) + ) + ); + + const oracleDelay = slot.sub(oraclePriceData.slot).sub(oracleStalenessBuffer); + + let isStaleForAmmImmediate = true; + if (market.amm.oracleSlotDelayOverride != 0) { + isStaleForAmmImmediate = oracleDelay.gt( + BN.max(new BN(market.amm.oracleSlotDelayOverride), ZERO) + ); + } + + let isStaleForAmmLowRisk = false; + if (market.amm.oracleLowRiskSlotDelayOverride != 0) { + isStaleForAmmLowRisk = oracleDelay.gt( + BN.max(new BN(market.amm.oracleLowRiskSlotDelayOverride), ZERO) + ); + } else { + isStaleForAmmLowRisk = oracleDelay.gt( + oracleGuardRails.validity.slotsBeforeStaleForAmm + ); + } + + let isStaleForMargin = oracleDelay.gt( + new BN(oracleGuardRails.validity.slotsBeforeStaleForMargin) + ); + if ( + isOneOfVariant(market.amm.oracleSource, [ + 'pythStableCoinPull', + 'pythLazerStableCoin', + ]) + ) { + isStaleForMargin = oracleDelay.gt( + new BN(oracleGuardRails.validity.slotsBeforeStaleForMargin).muln(3) + ); + } + + if (isNonPositive) { + return OracleValidity.NonPositive; + } else if (isTooVolatile) { + return OracleValidity.TooVolatile; + } else if (isConfTooLarge) { + return OracleValidity.TooUncertain; + } else if (isStaleForMargin) { + return OracleValidity.StaleForMargin; + } else if (!oraclePriceData.hasSufficientNumberOfDataPoints) { + return OracleValidity.InsufficientDataPoints; + } else if (isStaleForAmmLowRisk) { + return OracleValidity.StaleForAMMLowRisk; + } else if (isStaleForAmmImmediate) { + return OracleValidity.isStaleForAmmImmediate; + } else { + return OracleValidity.Valid; + } +} + export function isOracleValid( market: PerpMarketAccount, oraclePriceData: OraclePriceData, diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 2dc9196ab8..6886821be7 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -8,6 +8,8 @@ import { PositionDirection, ProtectedMakerParams, MarketTypeStr, + OrderBitFlag, + StateAccount, } from '../types'; import { ZERO, @@ -243,10 +245,16 @@ export function isFillableByVAMM( mmOraclePriceData: MMOraclePriceData, slot: number, ts: number, - minAuctionDuration: number + state: StateAccount ): boolean { return ( - (isFallbackAvailableLiquiditySource(order, minAuctionDuration, slot) && + (isFallbackAvailableLiquiditySource( + order, + mmOraclePriceData, + slot, + state, + market + ) && calculateBaseAssetAmountForAmmToFulfill( order, market, @@ -257,6 +265,26 @@ export function isFillableByVAMM( ); } +export function isLowRiskForAmm( + order: Order, + mmOraclePriceData: MMOraclePriceData, + isLiquidation?: boolean +): boolean { + if (isVariant(order.marketType, 'spot')) { + return false; + } + + const orderOlderThanOracleDelay = new BN(order.slot).lte( + mmOraclePriceData.slot + ); + + return ( + orderOlderThanOracleDelay || + isLiquidation || + (order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0 + ); +} + export function calculateBaseAssetAmountForAmmToFulfill( order: Order, market: PerpMarketAccount, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index fd0cb165cd..e2801414ed 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1107,7 +1107,8 @@ export type AMM = { quoteAssetAmountWithUnsettledLp: BN; referencePriceOffset: number; - takerSpeedBumpOverride: number; + oracleLowRiskSlotDelayOverride: number; + oracleSlotDelayOverride: number; ammSpreadAdjustment: number; ammInventorySpreadAdjustment: number; @@ -1473,6 +1474,17 @@ export type OracleGuardRails = { }; }; +export enum OracleValidity { + NonPositive = 0, + TooVolatile = 1, + TooUncertain = 2, + StaleForMargin = 3, + InsufficientDataPoints = 4, + StaleForAMMLowRisk = 5, + isStaleForAmmImmediate = 6, + Valid = 7, +} + export type PrelaunchOracle = { price: BN; maxPrice: BN; diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d682f5e757..287c77d725 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -145,7 +145,7 @@ export const mockAMM: AMM = { quoteAssetAmountWithUnsettledLp: new BN(0), referencePriceOffset: 0, - takerSpeedBumpOverride: 0, + oracleLowRiskSlotDelayOverride: 0, ammSpreadAdjustment: 0, ammInventorySpreadAdjustment: 0, mmOracleSequenceId: new BN(0), @@ -672,9 +672,9 @@ export class MockUserMap implements UserMapInterface { }); } - public async subscribe(): Promise {} + public async subscribe(): Promise { } - public async unsubscribe(): Promise {} + public async unsubscribe(): Promise { } public async addPubkey(userAccountPublicKey: PublicKey): Promise { const user = new User({ @@ -733,7 +733,7 @@ export class MockUserMap implements UserMapInterface { ); } - public async updateWithOrderRecord(_record: OrderRecord): Promise {} + public async updateWithOrderRecord(_record: OrderRecord): Promise { } public values(): IterableIterator { return this.userMap.values(); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index b3a933eb18..9c46994d99 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -219,6 +219,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 413000); + assert(cus < 415000); }); });