From a7f3bebb730b2660d1d6d34eba2a148f93ff6ef6 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 28 Oct 2025 12:31:39 -0400 Subject: [PATCH 1/9] init --- programs/drift/src/controller/liquidation.rs | 10 +++-- programs/drift/src/state/perp_market.rs | 42 +++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9503f24966..c321633982 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -367,6 +367,8 @@ pub fn liquidate_perp( )?; } + let liquidator_transfer_price = market.get_liquidation_transfer_price(oracle_price, user.perp_positions[position_index].get_direction())?; + drop(market); drop(quote_spot_market); @@ -411,13 +413,13 @@ pub fn liquidate_perp( // Make sure liquidator enters at better than limit price if let Some(limit_price) = limit_price { // calculate fee in price terms - let oracle_price_u128 = oracle_price.cast::()?; - let fee = oracle_price_u128 + let transfer_price_u128 = liquidator_transfer_price.cast::()?; + let fee = transfer_price_u128 .safe_mul(liquidator_fee.cast()?)? .safe_div(LIQUIDATION_FEE_PRECISION_U128)?; match user.perp_positions[position_index].get_direction() { PositionDirection::Long => { - let transfer_price = oracle_price_u128.safe_sub(fee)?; + let transfer_price = transfer_price_u128.safe_sub(fee)?; validate!( transfer_price <= limit_price.cast()?, ErrorCode::LiquidationDoesntSatisfyLimitPrice, @@ -427,7 +429,7 @@ pub fn liquidate_perp( )? } PositionDirection::Short => { - let transfer_price = oracle_price_u128.safe_add(fee)?; + let transfer_price = transfer_price_u128.safe_add(fee)?; validate!( transfer_price >= limit_price.cast()?, ErrorCode::LiquidationDoesntSatisfyLimitPrice, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 15d6462c83..351b96425c 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -740,15 +740,7 @@ impl PerpMarket { let last_fill_price = self.last_fill_price; - let mark_price_5min_twap = self.amm.last_mark_price_twap_5min; - 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)?.cast::()?; + let oracle_plus_basis_5min = self.get_oracle_plus_basis_5min(oracle_price)?; let last_funding_basis = self.get_last_funding_basis(oracle_price, now)?; @@ -783,6 +775,21 @@ impl PerpMarket { self.clamp_trigger_price(oracle_price.unsigned_abs(), median_price) } + fn get_oracle_plus_basis_5min(&self, oracle_price: i64) -> DriftResult { + let mark_price_5min_twap = self.amm.last_mark_price_twap_5min; + let last_oracle_price_twap_5min = + self.amm.historical_oracle_data.last_oracle_price_twap_5min; + + if mark_price_5min_twap > 0 && last_oracle_price_twap_5min > 0 { + let basis_5min = mark_price_5min_twap + .cast::()? + .safe_sub(last_oracle_price_twap_5min)?; + oracle_price.safe_add(basis_5min)?.cast::() + } else { + oracle_price.cast::() + } + } + #[inline(always)] fn get_last_funding_basis(&self, oracle_price: i64, now: i64) -> DriftResult { if self.amm.last_funding_oracle_twap > 0 { @@ -836,6 +843,23 @@ impl PerpMarket { )) } + pub fn get_liquidation_transfer_price( + &self, + oracle_price: i64, + direction: PositionDirection, + ) -> DriftResult { + let oracle_plus_basis_5min = self.get_oracle_plus_basis_5min(oracle_price)?; + + let oracle_price_abs = oracle_price.unsigned_abs(); + let take_over_price = if direction == PositionDirection::Long { + oracle_plus_basis_5min.min(oracle_price_abs) + } else { + oracle_plus_basis_5min.max(oracle_price_abs) + }; + + self.clamp_trigger_price(oracle_price_abs, take_over_price) + } + #[inline(always)] pub fn get_mm_oracle_price_data( &self, From da02f3cbdf29eef21ee7ff150b7dc89bc6e9caa7 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:04:11 -0400 Subject: [PATCH 2/9] added to liquidation fn --- programs/drift/src/controller/liquidation.rs | 36 ++- .../drift/src/controller/liquidation/tests.rs | 297 ++++++++++++++++++ programs/drift/src/math/liquidation.rs | 35 ++- programs/drift/src/math/liquidation/tests.rs | 62 ++++ programs/drift/src/state/perp_market.rs | 15 +- 5 files changed, 421 insertions(+), 24 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index c321633982..9859c11efb 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -32,8 +32,9 @@ use crate::math::liquidation::{ calculate_liability_transfer_implied_by_asset_amount, calculate_liability_transfer_to_cover_margin_shortage, calculate_liquidation_multiplier, calculate_max_pct_to_liquidate, calculate_perp_if_fee, calculate_spot_if_fee, - get_liquidation_fee, get_liquidation_order_params, validate_swap_within_liquidation_boundaries, - validate_transfer_satisfies_limit_price, LiquidationMultiplierType, + calculate_transfer_price_as_fee, get_liquidation_fee, get_liquidation_order_params, + validate_swap_within_liquidation_boundaries, validate_transfer_satisfies_limit_price, + LiquidationMultiplierType, }; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, @@ -340,6 +341,12 @@ pub fn liquidate_perp( slot, )?; + let transfer_price = market.get_liquidation_transfer_price( + oracle_price, + user.perp_positions[position_index].get_direction(), + )?; + let transfer_price_fee = calculate_transfer_price_as_fee(transfer_price, oracle_price)?; + let if_liquidation_fee = calculate_perp_if_fee( intermediate_margin_calculation.tracked_market_margin_shortage(margin_shortage)?, user_base_asset_amount, @@ -348,6 +355,7 @@ pub fn liquidate_perp( oracle_price, quote_oracle_price, market.if_liquidation_fee, + transfer_price_fee, )?; let mut base_asset_amount_to_cover_margin_shortage = @@ -358,6 +366,7 @@ pub fn liquidate_perp( if_liquidation_fee, oracle_price, quote_oracle_price, + transfer_price_fee, )?; if base_asset_amount_to_cover_margin_shortage != u64::MAX { @@ -367,8 +376,6 @@ pub fn liquidate_perp( )?; } - let liquidator_transfer_price = market.get_liquidation_transfer_price(oracle_price, user.perp_positions[position_index].get_direction())?; - drop(market); drop(quote_spot_market); @@ -413,7 +420,7 @@ pub fn liquidate_perp( // Make sure liquidator enters at better than limit price if let Some(limit_price) = limit_price { // calculate fee in price terms - let transfer_price_u128 = liquidator_transfer_price.cast::()?; + let transfer_price_u128 = transfer_price.cast::()?; let fee = transfer_price_u128 .safe_mul(liquidator_fee.cast()?)? .safe_div(LIQUIDATION_FEE_PRECISION_U128)?; @@ -441,9 +448,11 @@ pub fn liquidate_perp( } } - let base_asset_value = - calculate_base_asset_value_with_oracle_price(base_asset_amount.cast()?, oracle_price)? - .cast::()?; + let base_asset_value = calculate_base_asset_value_with_oracle_price( + base_asset_amount.cast()?, + transfer_price.cast()?, + )? + .cast::()?; let liquidator_fee = -base_asset_value .cast::()? @@ -949,6 +958,13 @@ pub fn liquidate_perp_with_fill( .get_price_data("e_spot_market.oracle_id())? .price; let liquidator_fee = market.liquidator_fee; + + let transfer_price = market.get_liquidation_transfer_price( + oracle_price, + user.perp_positions[position_index].get_direction(), + )?; + let transfer_price_fee = calculate_transfer_price_as_fee(transfer_price, oracle_price)?; + let if_liquidation_fee = calculate_perp_if_fee( intermediate_margin_calculation.tracked_market_margin_shortage(margin_shortage)?, user_base_asset_amount, @@ -957,6 +973,7 @@ pub fn liquidate_perp_with_fill( oracle_price, quote_oracle_price, market.if_liquidation_fee, + transfer_price_fee, )?; let base_asset_amount_to_cover_margin_shortage = standardize_base_asset_amount_ceil( calculate_base_asset_amount_to_cover_margin_shortage( @@ -966,6 +983,7 @@ pub fn liquidate_perp_with_fill( if_liquidation_fee, oracle_price, quote_oracle_price, + transfer_price_fee, )?, market.amm.order_step_size, )?; @@ -1025,7 +1043,7 @@ pub fn liquidate_perp_with_fill( market_index, existing_direction, base_asset_amount, - oracle_price, + transfer_price, liquidator_fee_adjusted, )?; diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index e8cf21acde..b404d98e9f 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2375,6 +2375,303 @@ pub mod liquidate_perp { let market_after = perp_market_map.get_ref(&0).unwrap(); assert_eq!(market_after.amm.total_liquidation_fee, 750000) } + + #[test] + pub fn successful_liquidation_long_perp_with_negative_basis_5min() { + 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(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + 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, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + last_mark_price_twap_5min: 100 * PRICE_PRECISION_U64 * 999 / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..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 user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -150 * QUOTE_PRECISION_I64, + quote_entry_amount: -150 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: [SpotPosition::default(); 8], + + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].base_asset_amount, 0); + assert_eq!(user.perp_positions[0].quote_asset_amount, -51099000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + assert_eq!( + liquidator.perp_positions[0].base_asset_amount, + BASE_PRECISION_I64 + ); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -98901000); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + } + + #[test] + pub fn successful_liquidation_short_perp_with_positive_basis_5min() { + 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(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + 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, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: 50 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + last_mark_price_twap_5min: 100 * PRICE_PRECISION_U64 * 1001 / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + funding_period: 3600, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_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, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..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 user = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: 50 * QUOTE_PRECISION_I64, + quote_entry_amount: 50 * QUOTE_PRECISION_I64, + quote_break_even_amount: 50 * QUOTE_PRECISION_I64, + open_orders: 1, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: [SpotPosition::default(); 8], + + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + let mut liquidator_stats = UserStats::default(); + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + BASE_PRECISION_U64, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].base_asset_amount, 0); + assert_eq!(user.perp_positions[0].quote_asset_amount, -51101000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + assert_eq!( + liquidator.perp_positions[0].base_asset_amount, + -BASE_PRECISION_I64 + ); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 101101000); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + } } pub mod liquidate_perp_with_fill { diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 24a54afc59..719e742a25 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -38,6 +38,7 @@ pub fn calculate_base_asset_amount_to_cover_margin_shortage( if_liquidation_fee: u32, oracle_price: i64, quote_oracle_price: i64, + transfer_price_fee: u32, ) -> DriftResult { let margin_ratio = margin_ratio.safe_mul(LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO)?; @@ -52,7 +53,12 @@ pub fn calculate_base_asset_amount_to_cover_margin_shortage( .cast::()? .safe_mul(quote_oracle_price.cast()?)? .safe_div(PRICE_PRECISION)? - .safe_mul(margin_ratio.safe_sub(liquidation_fee)?.cast()?)? + .safe_mul( + margin_ratio + .safe_sub(liquidation_fee)? + .safe_sub(transfer_price_fee.cast()?)? + .cast()?, + )? .safe_div(LIQUIDATION_FEE_PRECISION_U128)? .safe_sub( oracle_price @@ -369,6 +375,7 @@ pub fn calculate_perp_if_fee( oracle_price: i64, quote_oracle_price: i64, max_if_liquidation_fee: u32, + transfer_price_fee: u32, ) -> DriftResult { let margin_ratio = margin_ratio.safe_mul(LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO)?; @@ -385,9 +392,10 @@ pub fn calculate_perp_if_fee( .safe_mul(quote_oracle_price.cast()?)? .safe_div(PRICE_PRECISION)?; - // margin ratio - liquidator fee - (margin shortage / (user base asset amount * price)) + // margin ratio - liquidator fee - transfer price fee - (margin shortage / (user base asset amount * price)) let implied_if_fee = margin_ratio .saturating_sub(liquidator_fee) + .saturating_sub(transfer_price_fee) .saturating_sub( margin_shortage .safe_mul(BASE_PRECISION)? @@ -458,23 +466,23 @@ pub fn get_liquidation_order_params( market_index: u16, existing_direction: PositionDirection, base_asset_amount: u64, - oracle_price: i64, + transfer_price: u64, liquidation_fee: u32, ) -> DriftResult { let direction = existing_direction.opposite(); - let oracle_price_u128 = oracle_price.abs().cast::()?; + let transfer_price_u128 = transfer_price.cast::()?; let limit_price = match direction { - PositionDirection::Long => oracle_price_u128 + PositionDirection::Long => transfer_price_u128 .safe_add( - oracle_price_u128 + transfer_price_u128 .safe_mul(liquidation_fee.cast()?)? .safe_div(LIQUIDATION_FEE_PRECISION_U128)?, )? .cast::()?, - PositionDirection::Short => oracle_price_u128 + PositionDirection::Short => transfer_price_u128 .safe_sub( - oracle_price_u128 + transfer_price_u128 .safe_mul(liquidation_fee.cast()?)? .safe_div(LIQUIDATION_FEE_PRECISION_U128)?, )? @@ -551,3 +559,14 @@ pub fn validate_swap_within_liquidation_boundaries( Ok(()) } + +pub fn calculate_transfer_price_as_fee(transfer_price: u64, oracle_price: i64) -> DriftResult { + let delta = oracle_price.safe_sub(transfer_price.cast()?)?.abs(); + let fee = delta + .cast::()? + .safe_mul(LIQUIDATION_FEE_PRECISION_U128)? + .safe_div(oracle_price.cast::()?)? + .cast::() + .unwrap_or(u32::MAX); + Ok(fee) +} diff --git a/programs/drift/src/math/liquidation/tests.rs b/programs/drift/src/math/liquidation/tests.rs index 17315dabbf..caf8e92e85 100644 --- a/programs/drift/src/math/liquidation/tests.rs +++ b/programs/drift/src/math/liquidation/tests.rs @@ -19,6 +19,7 @@ mod calculate_base_asset_amount_to_cover_margin_shortage { 0, oracle_price, PRICE_PRECISION_I64, + 0, ) .unwrap(); @@ -39,6 +40,7 @@ mod calculate_base_asset_amount_to_cover_margin_shortage { 0, oracle_price, quote_oracle_price, + 0, ) .unwrap(); @@ -52,6 +54,7 @@ mod calculate_base_asset_amount_to_cover_margin_shortage { 0, oracle_price, quote_oracle_price, + 0, ) .unwrap(); @@ -71,6 +74,7 @@ mod calculate_base_asset_amount_to_cover_margin_shortage { 0, oracle_price, PRICE_PRECISION_I64, + 0, ) .unwrap(); @@ -91,6 +95,41 @@ mod calculate_base_asset_amount_to_cover_margin_shortage { assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 10 / 9); // must lose 10/9 base } + #[test] + pub fn one_percent_liquidation_fee_and_one_percent_transfer_fee() { + let margin_shortage = 10 * QUOTE_PRECISION; // $10 shortage + let margin_ratio = MARGIN_PRECISION / 10; // 10x leverage + let liquidation_fee = LIQUIDATION_FEE_PRECISION / 100; // 1 percent + let transfer_fee = LIQUIDATION_FEE_PRECISION / 100; // 1 percent + let oracle_price = 100 * PRICE_PRECISION_I64; // $100 / base + let base_asset_amount = calculate_base_asset_amount_to_cover_margin_shortage( + margin_shortage, + margin_ratio, + liquidation_fee, + 0, + oracle_price, + PRICE_PRECISION_I64, + transfer_fee, + ) + .unwrap(); + + let freed_collateral = (base_asset_amount as u128) * (oracle_price as u128) + / PRICE_PRECISION + / AMM_TO_QUOTE_PRECISION_RATIO + * margin_ratio as u128 + / MARGIN_PRECISION_U128; + + let negative_pnl = (base_asset_amount as u128) * (oracle_price as u128) + / PRICE_PRECISION + / AMM_TO_QUOTE_PRECISION_RATIO + * (liquidation_fee as u128 + transfer_fee as u128) + / LIQUIDATION_FEE_PRECISION_U128; + + assert_eq!(freed_collateral - negative_pnl, 10000000); // ~$10 + + assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 125 / 100); // must lose 125/100 base + } + #[test] pub fn one_percent_liquidation_fee_and_one_percent_if_liquidation_fee() { let margin_shortage = 10 * QUOTE_PRECISION; // $10 shortage @@ -105,6 +144,7 @@ mod calculate_base_asset_amount_to_cover_margin_shortage { if_liquidation_fee, oracle_price, PRICE_PRECISION_I64, + 0, ) .unwrap(); @@ -660,11 +700,27 @@ mod calculate_perp_if_fee { oracle_price, quote_price, max_if_fee, + 0, ) .unwrap(); assert_eq!(fee, 19000); // 2% * .95 + let transfer_fee = LIQUIDATION_FEE_PRECISION / 100; // 1% + let fee = calculate_perp_if_fee( + margin_shortage, + base_asset_amount, + margin_ratio, + liquidator_fee, + oracle_price, + quote_price, + max_if_fee, + transfer_fee, + ) + .unwrap(); + + assert_eq!(fee, 9500); // 1% * .95 + let tiny_margin_shortage = QUOTE_PRECISION; let fee = calculate_perp_if_fee( tiny_margin_shortage, @@ -674,6 +730,7 @@ mod calculate_perp_if_fee { oracle_price, quote_price, max_if_fee, + 0, ) .unwrap(); @@ -688,6 +745,7 @@ mod calculate_perp_if_fee { oracle_price, quote_price, max_if_fee, + 0, ) .unwrap(); @@ -702,6 +760,7 @@ mod calculate_perp_if_fee { oracle_price, quote_price, max_if_fee, + 0, ) .unwrap(); @@ -716,6 +775,7 @@ mod calculate_perp_if_fee { zero_oracle_price, quote_price, max_if_fee, + 0, ) .unwrap(); @@ -730,6 +790,7 @@ mod calculate_perp_if_fee { oracle_price, zero_quote_price, max_if_fee, + 0, ) .unwrap(); @@ -744,6 +805,7 @@ mod calculate_perp_if_fee { oracle_price, quote_price, max_if_fee, + 0, ) .unwrap(); diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 351b96425c..5e297e9d98 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -780,14 +780,15 @@ impl PerpMarket { let last_oracle_price_twap_5min = self.amm.historical_oracle_data.last_oracle_price_twap_5min; - if mark_price_5min_twap > 0 && last_oracle_price_twap_5min > 0 { - let basis_5min = mark_price_5min_twap - .cast::()? - .safe_sub(last_oracle_price_twap_5min)?; - oracle_price.safe_add(basis_5min)?.cast::() - } else { - oracle_price.cast::() + if mark_price_5min_twap == 0 || last_oracle_price_twap_5min == 0 { + return Ok(oracle_price.cast::()?); } + + let basis_5min = mark_price_5min_twap + .cast::()? + .safe_sub(last_oracle_price_twap_5min)?; + + oracle_price.safe_add(basis_5min)?.cast::() } #[inline(always)] From 59bc60bf7d058b587a8675af0dbd5f26a1020b6c Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:07:41 -0400 Subject: [PATCH 3/9] remove min/max with liquidation price --- programs/drift/src/state/perp_market.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 5e297e9d98..77577555ea 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -852,13 +852,8 @@ impl PerpMarket { let oracle_plus_basis_5min = self.get_oracle_plus_basis_5min(oracle_price)?; let oracle_price_abs = oracle_price.unsigned_abs(); - let take_over_price = if direction == PositionDirection::Long { - oracle_plus_basis_5min.min(oracle_price_abs) - } else { - oracle_plus_basis_5min.max(oracle_price_abs) - }; - self.clamp_trigger_price(oracle_price_abs, take_over_price) + self.clamp_trigger_price(oracle_price_abs, oracle_plus_basis_5min) } #[inline(always)] From 9a6eed6cdc451f3a7547aa4c0d6db73ef500ded6 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:09:38 -0400 Subject: [PATCH 4/9] rm min/max --- programs/drift/src/controller/liquidation.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9859c11efb..56f218ef7e 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -341,10 +341,7 @@ pub fn liquidate_perp( slot, )?; - let transfer_price = market.get_liquidation_transfer_price( - oracle_price, - user.perp_positions[position_index].get_direction(), - )?; + let transfer_price = market.get_liquidation_transfer_price(oracle_price)?; let transfer_price_fee = calculate_transfer_price_as_fee(transfer_price, oracle_price)?; let if_liquidation_fee = calculate_perp_if_fee( @@ -959,10 +956,7 @@ pub fn liquidate_perp_with_fill( .price; let liquidator_fee = market.liquidator_fee; - let transfer_price = market.get_liquidation_transfer_price( - oracle_price, - user.perp_positions[position_index].get_direction(), - )?; + let transfer_price = market.get_liquidation_transfer_price(oracle_price)?; let transfer_price_fee = calculate_transfer_price_as_fee(transfer_price, oracle_price)?; let if_liquidation_fee = calculate_perp_if_fee( From 19bfda21d2eede1b49fd433097a47cba8285da2e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:11:30 -0400 Subject: [PATCH 5/9] misisng commit --- programs/drift/src/state/perp_market.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 77577555ea..9f41958ab6 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -847,7 +847,6 @@ impl PerpMarket { pub fn get_liquidation_transfer_price( &self, oracle_price: i64, - direction: PositionDirection, ) -> DriftResult { let oracle_plus_basis_5min = self.get_oracle_plus_basis_5min(oracle_price)?; From 84bb6cd1dde1a99252df7c51c199fb51f70e5586 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:25:27 -0400 Subject: [PATCH 6/9] Revert "misisng commit" This reverts commit 19bfda21d2eede1b49fd433097a47cba8285da2e. --- programs/drift/src/state/perp_market.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 9f41958ab6..77577555ea 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -847,6 +847,7 @@ impl PerpMarket { pub fn get_liquidation_transfer_price( &self, oracle_price: i64, + direction: PositionDirection, ) -> DriftResult { let oracle_plus_basis_5min = self.get_oracle_plus_basis_5min(oracle_price)?; From 83c7529a50e63b748e3084712be9ee04c04f1767 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:25:30 -0400 Subject: [PATCH 7/9] Revert "rm min/max" This reverts commit 9a6eed6cdc451f3a7547aa4c0d6db73ef500ded6. --- programs/drift/src/controller/liquidation.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 56f218ef7e..9859c11efb 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -341,7 +341,10 @@ pub fn liquidate_perp( slot, )?; - let transfer_price = market.get_liquidation_transfer_price(oracle_price)?; + let transfer_price = market.get_liquidation_transfer_price( + oracle_price, + user.perp_positions[position_index].get_direction(), + )?; let transfer_price_fee = calculate_transfer_price_as_fee(transfer_price, oracle_price)?; let if_liquidation_fee = calculate_perp_if_fee( @@ -956,7 +959,10 @@ pub fn liquidate_perp_with_fill( .price; let liquidator_fee = market.liquidator_fee; - let transfer_price = market.get_liquidation_transfer_price(oracle_price)?; + let transfer_price = market.get_liquidation_transfer_price( + oracle_price, + user.perp_positions[position_index].get_direction(), + )?; let transfer_price_fee = calculate_transfer_price_as_fee(transfer_price, oracle_price)?; let if_liquidation_fee = calculate_perp_if_fee( From 93349fc10251901de9dd61aa05b39494e68c95d8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 15:25:35 -0400 Subject: [PATCH 8/9] Revert "remove min/max with liquidation price" This reverts commit 59bc60bf7d058b587a8675af0dbd5f26a1020b6c. --- programs/drift/src/state/perp_market.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 77577555ea..5e297e9d98 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -852,8 +852,13 @@ impl PerpMarket { let oracle_plus_basis_5min = self.get_oracle_plus_basis_5min(oracle_price)?; let oracle_price_abs = oracle_price.unsigned_abs(); + let take_over_price = if direction == PositionDirection::Long { + oracle_plus_basis_5min.min(oracle_price_abs) + } else { + oracle_plus_basis_5min.max(oracle_price_abs) + }; - self.clamp_trigger_price(oracle_price_abs, oracle_plus_basis_5min) + self.clamp_trigger_price(oracle_price_abs, take_over_price) } #[inline(always)] From 897d921f1f066061a1394182379ed1b02e7136af Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 4 Nov 2025 15:42:44 -0500 Subject: [PATCH 9/9] tweak --- programs/drift/src/math/liquidation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 719e742a25..d18252e336 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -566,7 +566,7 @@ pub fn calculate_transfer_price_as_fee(transfer_price: u64, oracle_price: i64) - .cast::()? .safe_mul(LIQUIDATION_FEE_PRECISION_U128)? .safe_div(oracle_price.cast::()?)? - .cast::() - .unwrap_or(u32::MAX); + .cast::()?; + Ok(fee) }