diff --git a/programs/drift/src/controller/funding.rs b/programs/drift/src/controller/funding.rs index a66d7c9cc3..ed9585580a 100644 --- a/programs/drift/src/controller/funding.rs +++ b/programs/drift/src/controller/funding.rs @@ -269,6 +269,7 @@ pub fn update_funding_rate( .safe_add(funding_rate_short)?; market.amm.last_funding_rate = funding_rate; + market.amm.last_funding_oracle_twap = oracle_price_twap; market.amm.last_funding_rate_long = funding_rate_long.cast()?; market.amm.last_funding_rate_short = funding_rate_short.cast()?; market.amm.last_24h_avg_funding_rate = calculate_new_twap( diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index d470c4757b..8d0e2e7e37 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -691,6 +691,7 @@ pub fn liquidate_perp( taker_existing_base_asset_amount: taker_existing_base_asset_amount, maker_existing_quote_entry_amount: maker_existing_quote_entry_amount, maker_existing_base_asset_amount: maker_existing_base_asset_amount, + trigger_price: None, }; emit!(fill_record); diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 5d9818172b..92fec1560c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -428,6 +428,7 @@ pub fn place_perp_order( None, None, None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -707,6 +708,7 @@ pub fn cancel_order( None, None, None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; } @@ -1295,7 +1297,7 @@ pub fn fill_perp_order( let fill_price = calculate_fill_price(quote_asset_amount, base_asset_amount, BASE_PRECISION_U64)?; - let perp_market = perp_market_map.get_ref(&market_index)?; + let mut perp_market = perp_market_map.get_ref_mut(&market_index)?; validate_fill_price_within_price_bands( fill_price, order_direction, @@ -1307,6 +1309,8 @@ pub fn fill_perp_order( .max_oracle_twap_5min_percent_divergence(), perp_market.is_prediction_market(), )?; + + perp_market.last_fill_price = fill_price; } let base_asset_amount_after = user.perp_positions[position_index].base_asset_amount; @@ -2419,6 +2423,7 @@ pub fn fulfill_perp_order_with_amm( taker_existing_base_asset_amount, maker_existing_quote_entry_amount, maker_existing_base_asset_amount, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2864,6 +2869,7 @@ pub fn fulfill_perp_order_with_match( taker_existing_base_asset_amount, maker_existing_quote_entry_amount, maker_existing_base_asset_amount, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -3016,10 +3022,8 @@ pub fn trigger_order( "oracle price vs twap too divergent" )?; - let can_trigger = order_satisfies_trigger_condition( - &user.orders[order_index], - oracle_price.unsigned_abs().cast()?, - )?; + let trigger_price = perp_market.get_trigger_price(oracle_price, now)?; + let can_trigger = order_satisfies_trigger_condition(&user.orders[order_index], trigger_price)?; validate!(can_trigger, ErrorCode::OrderDidNotSatisfyTriggerCondition)?; let (_, worst_case_liability_value_before) = user @@ -3086,6 +3090,7 @@ pub fn trigger_order( None, None, None, + Some(trigger_price), )?; emit!(order_action_record); @@ -3761,6 +3766,7 @@ pub fn place_spot_order( None, None, None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -4992,6 +4998,7 @@ pub fn fulfill_spot_order_with_match( None, None, None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5262,6 +5269,7 @@ pub fn fulfill_spot_order_with_external_market( None, None, None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5459,6 +5467,7 @@ pub fn trigger_spot_order( None, None, None, + Some(oracle_price.unsigned_abs()), )?; emit!(order_action_record); diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 47f7a92824..b05a36c4f1 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -955,7 +955,9 @@ pub fn handle_initialize_perp_market( high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding: [0; 36], + padding1: 0, + last_fill_price: 0, + padding: [0; 24], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, @@ -1052,7 +1054,8 @@ pub fn handle_initialize_perp_market( quote_asset_amount_with_unsettled_lp: 0, reference_price_offset: 0, amm_inventory_spread_adjustment: 0, - padding: [0; 11], + padding: [0; 3], + last_funding_oracle_twap: 0, }, }; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 0b4307c639..e5bebbb865 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1874,6 +1874,7 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( taker_existing_base_asset_amount: to_existing_base_asset_amount, maker_existing_quote_entry_amount: from_existing_quote_entry_amount, maker_existing_base_asset_amount: from_existing_base_asset_amount, + trigger_price: None, }; emit_stack::<_, { OrderActionRecord::SIZE }>(fill_record)?; diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 85a2944139..27fb5ef66c 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -204,6 +204,8 @@ pub const DEFAULT_QUOTE_ASSET_AMOUNT_TICK_SIZE: u64 = // FUNDING pub const FUNDING_RATE_OFFSET_DENOMINATOR: i64 = 5000; // 5000 => 7.3% annualized rate for hourly funding +pub const FUNDING_RATE_OFFSET_PERCENTAGE: i64 = + FUNDING_RATE_PRECISION_I64 / FUNDING_RATE_OFFSET_DENOMINATOR; // ORDERS pub const AUCTION_DERIVE_PRICE_FRACTION: i64 = 200; diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 1493cee987..1060895d31 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -254,10 +254,12 @@ pub struct OrderActionRecord { /// precision: BASE_PRECISION /// Only Some if the maker flipped position direction pub maker_existing_base_asset_amount: Option, + /// precision: PRICE_PRECISION + pub trigger_price: Option, } impl Size for OrderActionRecord { - const SIZE: usize = 448; + const SIZE: usize = 464; } pub fn get_order_action_record( @@ -285,6 +287,7 @@ pub fn get_order_action_record( taker_existing_base_asset_amount: Option, maker_existing_quote_entry_amount: Option, maker_existing_base_asset_amount: Option, + trigger_price: Option, ) -> DriftResult { Ok(OrderActionRecord { ts, @@ -337,6 +340,7 @@ pub fn get_order_action_record( taker_existing_base_asset_amount, maker_existing_quote_entry_amount, maker_existing_base_asset_amount, + trigger_price, }) } diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 8983162b7b..2e0f2a7ee0 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -9,17 +9,17 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{ - AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, -}; +use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; use crate::math::constants::{ AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, - DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, LIQUIDATION_FEE_PRECISION, + DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, FUNDING_RATE_BUFFER_I128, + FUNDING_RATE_OFFSET_PERCENTAGE, LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, LP_FEE_SLICE_DENOMINATOR, LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION, MARGIN_PRECISION_U128, MAX_LIQUIDATION_MULTIPLIER, PEG_PRECISION, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, PRICE_PRECISION_I64, + SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; use crate::math::helpers::get_proportion_i128; use crate::math::margin::{ @@ -251,7 +251,9 @@ pub struct PerpMarket { pub high_leverage_margin_ratio_maintenance: u16, pub protected_maker_limit_price_divisor: u8, pub protected_maker_dynamic_divisor: u8, - pub padding: [u8; 36], + pub padding1: u32, + pub last_fill_price: u64, + pub padding: [u8; 24], } impl Default for PerpMarket { @@ -293,7 +295,9 @@ impl Default for PerpMarket { high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding: [0; 36], + padding1: 0, + last_fill_price: 0, + padding: [0; 24], } } } @@ -747,6 +751,96 @@ impl PerpMarket { default_min_auction_duration } } + + pub fn get_trigger_price(&self, oracle_price: i64, now: i64) -> DriftResult { + let last_fill_price = self.last_fill_price; + + let mark_price_5min_twap = self.amm.last_mark_price_twap; + let last_oracle_price_twap_5min = + self.amm.historical_oracle_data.last_oracle_price_twap_5min; + + let basis_5min = mark_price_5min_twap + .cast::()? + .safe_sub(last_oracle_price_twap_5min)?; + + let oracle_plus_basis_5min = oracle_price.safe_add(basis_5min)?.unsigned_abs(); + + let last_funding_basis = self.get_last_funding_basis(oracle_price, now)?; + + let oracle_plus_funding_basis = oracle_price.safe_add(last_funding_basis)?.unsigned_abs(); + + let median_price = if last_fill_price > 0 { + let mut prices = [ + last_fill_price, + oracle_plus_funding_basis, + oracle_plus_basis_5min, + ]; + prices.sort_unstable(); + + prices[1] + } else { + let mut prices = [ + oracle_price.unsigned_abs(), + oracle_plus_funding_basis, + oracle_plus_basis_5min, + ]; + prices.sort_unstable(); + + prices[1] + }; + + self.clamp_trigger_price(oracle_price.unsigned_abs(), median_price) + } + + #[inline(always)] + fn get_last_funding_basis(&self, oracle_price: i64, now: i64) -> DriftResult { + if self.amm.last_funding_oracle_twap > 0 { + let last_funding_rate = self + .amm + .last_funding_rate + .cast::()? + .safe_mul(PRICE_PRECISION_I128)? + .safe_div(self.amm.last_funding_oracle_twap.cast::()?)? + .safe_mul(24)?; + let last_funding_rate_pre_adj = + last_funding_rate.safe_sub(FUNDING_RATE_OFFSET_PERCENTAGE as i128)?; + + let time_left_until_funding_update = now + .safe_sub(self.amm.last_funding_rate_ts)? + .min(self.amm.funding_period); + + let last_funding_basis = oracle_price + .cast::()? + .safe_mul(last_funding_rate_pre_adj)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul( + self.amm + .funding_period + .safe_sub(time_left_until_funding_update)? + .cast::()?, + )? + .safe_div(self.amm.funding_period.cast::()?)? + / FUNDING_RATE_BUFFER_I128; + + last_funding_basis.cast::() + } else { + Ok(0) + } + } + + #[inline(always)] + fn clamp_trigger_price(&self, oracle_price: u64, median_price: u64) -> DriftResult { + let max_bps_diff = if matches!(self.contract_tier, ContractTier::A | ContractTier::B) { + 500 // 20 BPS + } else { + 100 // 100 BPS + }; + let max_oracle_diff = oracle_price / max_bps_diff; + + Ok(median_price + .max(oracle_price.safe_sub(max_oracle_diff)?) + .min(oracle_price.safe_add(max_oracle_diff)?)) + } } #[cfg(test)] @@ -912,10 +1006,10 @@ pub struct AMM { /// precision: AMM_RESERVE_PRECISION pub user_lp_shares: u128, /// last funding rate in this perp market (unit is quote per base) - /// precision: QUOTE_PRECISION + /// precision: FUNDING_RATE_PRECISION pub last_funding_rate: i64, /// last funding rate for longs in this perp market (unit is quote per base) - /// precision: QUOTE_PRECISION + /// precision: FUNDING_RATE_PRECISION pub last_funding_rate_long: i64, /// last funding rate for shorts in this perp market (unit is quote per base) /// precision: QUOTE_PRECISION @@ -1058,7 +1152,8 @@ pub struct AMM { pub reference_price_offset: i32, /// signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double) pub amm_inventory_spread_adjustment: i8, - pub padding: [u8; 11], + pub padding: [u8; 3], + pub last_funding_oracle_twap: i64, } impl Default for AMM { @@ -1149,7 +1244,8 @@ impl Default for AMM { quote_asset_amount_with_unsettled_lp: 0, reference_price_offset: 0, amm_inventory_spread_adjustment: 0, - padding: [0; 11], + padding: [0; 3], + last_funding_oracle_twap: 0, } } } diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index 3d3a9bb4cb..5b7ab38ae9 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -156,3 +156,181 @@ mod get_min_perp_auction_duration { ); } } + +mod get_trigger_price { + use crate::state::perp_market::HistoricalOracleData; + use crate::state::perp_market::{PerpMarket, AMM}; + + #[test] + fn test_get_last_funding_basis() { + let oracle_price = 109144736794; + let last_funding_rate_ts = 1752080410; + let now = last_funding_rate_ts + 0; + let perp_market = PerpMarket { + amm: AMM { + last_funding_rate: 1410520875, + last_funding_rate_ts: 1752080410, + last_mark_price_twap: 109146153042, + last_funding_oracle_twap: 109198342833, + funding_period: 3600, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap_5min: 109143803911, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + ..PerpMarket::default() + }; + + let last_funding_basis = perp_market + .get_last_funding_basis(oracle_price, now) + .unwrap(); + + assert_eq!(last_funding_basis, 12006794); // $12 basis + + let now = last_funding_rate_ts + 1800; + let last_funding_basis = perp_market + .get_last_funding_basis(oracle_price, now) + .unwrap(); + + assert_eq!(last_funding_basis, 6003397); // $6 basis + + let now = last_funding_rate_ts + 3600; + let last_funding_basis = perp_market + .get_last_funding_basis(oracle_price, now) + .unwrap(); + + assert_eq!(last_funding_basis, 0); + + let now = last_funding_rate_ts + 5400; + let last_funding_basis = perp_market + .get_last_funding_basis(oracle_price, now) + .unwrap(); + + assert_eq!(last_funding_basis, 0); + } + + #[test] + fn test_get_trigger_price() { + let oracle_price = 109144736794; + let now = 1752082210; + let perp_market = PerpMarket { + amm: AMM { + last_funding_rate: 1410520875, + last_funding_rate_ts: 1752080410, + last_mark_price_twap: 109146153042, + last_funding_oracle_twap: 109198342833, + funding_period: 3600, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap_5min: 109143803911, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + ..PerpMarket::default() + }; + + let trigger_price = perp_market.get_trigger_price(oracle_price, now).unwrap(); + + assert_eq!(trigger_price, 109147085925); + } + + #[test] + fn test_clamp_trigger_price() { + use crate::state::perp_market::{ContractTier, PerpMarket}; + + // Test Contract Tier A (20 BPS = 500 divisor) + let perp_market_a = PerpMarket { + contract_tier: ContractTier::A, + ..PerpMarket::default() + }; + + let oracle_price = 100_000_000_000; // $100,000 + let max_bps_diff = 500; // 20 BPS + let max_oracle_diff = oracle_price / max_bps_diff; // 200,000,000 + + // Test median price below lower bound + let median_price_below = oracle_price - max_oracle_diff - 1_000_000; + let clamped_price = perp_market_a + .clamp_trigger_price(oracle_price, median_price_below) + .unwrap(); + assert_eq!(clamped_price, oracle_price - max_oracle_diff); + + // Test median price above upper bound + let median_price_above = oracle_price + max_oracle_diff + 1_000_000; + let clamped_price = perp_market_a + .clamp_trigger_price(oracle_price, median_price_above) + .unwrap(); + assert_eq!(clamped_price, oracle_price + max_oracle_diff); + + // Test median price within bounds (should not be clamped) + let median_price_within = oracle_price + max_oracle_diff / 2; + let clamped_price = perp_market_a + .clamp_trigger_price(oracle_price, median_price_within) + .unwrap(); + assert_eq!(clamped_price, median_price_within); + + // Test median price at exact bounds + let median_price_at_lower = oracle_price - max_oracle_diff; + let clamped_price = perp_market_a + .clamp_trigger_price(oracle_price, median_price_at_lower) + .unwrap(); + assert_eq!(clamped_price, median_price_at_lower); + + let median_price_at_upper = oracle_price + max_oracle_diff; + let clamped_price = perp_market_a + .clamp_trigger_price(oracle_price, median_price_at_upper) + .unwrap(); + assert_eq!(clamped_price, median_price_at_upper); + + // Test Contract Tier C (100 BPS = 100 divisor) + let perp_market_c = PerpMarket { + contract_tier: ContractTier::C, + ..PerpMarket::default() + }; + + let max_bps_diff_c = 100; // 100 BPS + let max_oracle_diff_c = oracle_price / max_bps_diff_c; // 1,000,000,000 + + // Test median price below lower bound for Tier C + let median_price_below_c = oracle_price - max_oracle_diff_c - 1_000_000; + let clamped_price = perp_market_c + .clamp_trigger_price(oracle_price, median_price_below_c) + .unwrap(); + assert_eq!(clamped_price, oracle_price - max_oracle_diff_c); + + // Test median price above upper bound for Tier C + let median_price_above_c = oracle_price + max_oracle_diff_c + 1_000_000; + let clamped_price = perp_market_c + .clamp_trigger_price(oracle_price, median_price_above_c) + .unwrap(); + assert_eq!(clamped_price, oracle_price + max_oracle_diff_c); + + // Test median price within bounds for Tier C + let median_price_within_c = oracle_price + max_oracle_diff_c / 2; + let clamped_price = perp_market_c + .clamp_trigger_price(oracle_price, median_price_within_c) + .unwrap(); + assert_eq!(clamped_price, median_price_within_c); + + // Test edge cases with very small oracle price + let small_oracle_price = 1_000_000; // $1 + let max_oracle_diff_small = small_oracle_price / max_bps_diff; // 2,000 + + let median_price_small = small_oracle_price - max_oracle_diff_small - 100; + let clamped_price = perp_market_a + .clamp_trigger_price(small_oracle_price, median_price_small) + .unwrap(); + assert_eq!(clamped_price, small_oracle_price - max_oracle_diff_small); + + // Test edge cases with very large oracle price + let large_oracle_price = 1_000_000_000_000_000; // $1M + let max_oracle_diff_large = large_oracle_price / max_bps_diff; // 2,000,000,000,000 + + let median_price_large = large_oracle_price + max_oracle_diff_large + 1_000_000_000; + let clamped_price = perp_market_a + .clamp_trigger_price(large_oracle_price, median_price_large) + .unwrap(); + assert_eq!(clamped_price, large_oracle_price + max_oracle_diff_large); + } +}