diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6cff2d1350..669952732f 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1379,6 +1379,20 @@ pub fn fill_perp_order( )? } + if base_asset_amount_after == 0 && user.perp_positions[position_index].open_orders != 0 { + cancel_reduce_only_trigger_orders( + user, + &user_key, + Some(&filler_key), + perp_market_map, + spot_market_map, + oracle_map, + now, + slot, + market_index, + )?; + } + if base_asset_amount == 0 { return Ok((base_asset_amount, quote_asset_amount)); } @@ -2927,6 +2941,57 @@ fn get_taker_and_maker_for_order_record( } } +fn cancel_reduce_only_trigger_orders( + user: &mut User, + user_key: &Pubkey, + filler_key: Option<&Pubkey>, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + now: i64, + slot: u64, + perp_market_index: u16, +) -> DriftResult { + for order_index in 0..user.orders.len() { + if user.orders[order_index].status != OrderStatus::Open { + continue; + } + + if user.orders[order_index].market_type != MarketType::Perp { + continue; + } + + if user.orders[order_index].market_index != perp_market_index { + continue; + } + + if !user.orders[order_index].must_be_triggered() { + continue; + } + + if !user.orders[order_index].reduce_only { + continue; + } + + cancel_order( + order_index, + user, + user_key, + perp_market_map, + spot_market_map, + oracle_map, + now, + slot, + OrderActionExplanation::ReduceOnlyOrderIncreasedPosition, + filler_key, + 0, + false, + )?; + } + + Ok(()) +} + pub fn trigger_order( order_id: u32, state: &State, diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index b5e681f090..840cfdb9c9 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -10315,6 +10315,228 @@ pub mod force_cancel_orders { } } +pub mod cancel_reduce_only_trigger_orders { + use std::str::FromStr; + + use anchor_lang::prelude::Clock; + + use crate::controller::orders::cancel_reduce_only_trigger_orders; + use crate::controller::position::PositionDirection; + use crate::create_account_info; + use crate::create_anchor_account_info; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, LAMPORTS_PER_SOL_I64, PEG_PRECISION, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::state::oracle::HistoricalOracleData; + use crate::state::oracle::OracleSource; + use crate::state::perp_market::{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::state::State; + use crate::state::user::{MarketType, OrderStatus, OrderType, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::{ + create_account_info, get_positions, get_pyth_price, get_spot_positions, + }; + + use super::*; + + #[test] + fn test() { + let clock = Clock { + slot: 6, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + + 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, clock.slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + terminal_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: 100, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + max_spread: 1000, + base_spread: 0, + long_spread: 0, + short_spread: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: oracle_price.twap, + last_oracle_price_twap_5min: oracle_price.twap, + last_oracle_price: oracle_price.agg.price, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default() + }; + market.status = MarketStatus::Active; + market.amm.max_base_asset_reserve = u128::MAX; + 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 usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + deposit_balance: SPOT_BALANCE_PRECISION, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + + let mut sol_spot_market = SpotMarket { + market_index: 1, + deposit_balance: SPOT_BALANCE_PRECISION, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + oracle: oracle_price_key, + ..SpotMarket::default_base_market() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ], + true, + ) + .unwrap(); + + let mut orders = [Order::default(); 32]; + orders[0] = Order { + market_index: 0, + order_id: 1, + status: OrderStatus::Open, + order_type: OrderType::Limit, + market_type: MarketType::Perp, + ..Order::default() + }; + orders[1] = Order { + market_index: 1, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerMarket, + market_type: MarketType::Perp, + reduce_only: true, + ..Order::default() + }; + orders[2] = Order { + market_index: 0, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerMarket, + market_type: MarketType::Perp, + reduce_only: true, + ..Order::default() + }; + orders[3] = Order { + market_index: 0, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerMarket, + market_type: MarketType::Spot, + reduce_only: true, + ..Order::default() + }; + orders[4] = Order { + market_index: 0, + order_id: 2, + status: OrderStatus::Open, + order_type: OrderType::TriggerLimit, + market_type: MarketType::Perp, + reduce_only: true, + ..Order::default() + }; + + let mut user = User { + authority: Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(), // different authority than filler + orders, + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + open_orders: 2, + open_bids: 100 * BASE_PRECISION_I64, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Deposit, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + open_orders: 2, + open_bids: 100 * LAMPORTS_PER_SOL_I64, + open_asks: -LAMPORTS_PER_SOL_I64, + ..SpotPosition::default() + }), + ..User::default() + }; + + cancel_reduce_only_trigger_orders( + &mut user, + &Pubkey::default(), + Some(&Pubkey::default()), + &market_map, + &spot_market_map, + &mut oracle_map, + 0, + 0, + 0, + ) + .unwrap(); + + assert_eq!(user.orders[0].status, OrderStatus::Open); + assert_eq!(user.orders[1].status, OrderStatus::Open); + assert_eq!(user.orders[2].status, OrderStatus::Canceled); + assert_eq!(user.orders[3].status, OrderStatus::Open); + assert_eq!(user.orders[4].status, OrderStatus::Canceled); + } +} + pub mod insert_maker_order_info { use crate::controller::orders::insert_maker_order_info; use crate::controller::position::PositionDirection;