Skip to content

Commit 8d9f930

Browse files
pranay-hftpranay123-stack
authored andcommitted
Fix: Auto-cancel trigger orders when position is closed (#923)
Resolves issue #923 where stop-loss/take-profit (trigger) orders were not being automatically cancelled when a user closed their position. ## Problem When users closed their positions, trigger orders remained active, creating risks: - Unintended order execution after position closure - Opening new unwanted positions - Potential financial losses for users ## Solution Implemented automatic cancellation of trigger orders when positions are fully closed: ### Code Changes 1. **New Function**: `cancel_trigger_orders_for_closed_position()` (lines 587-668) - Iterates through all user orders - Identifies and cancels trigger orders (TriggerMarket/TriggerLimit) for a specific market - Properly documented with rustdoc comments 2. **Modified**: `fulfill_perp_order()` (lines 2128-2148) - Checks if perp position is completely closed (base_asset_amount == 0) - Automatically cancels all trigger orders for that market 3. **Modified**: `fulfill_spot_order()` (lines 4788-4808) - Checks if spot position is completely closed (scaled_balance == 0) - Automatically cancels all trigger orders for that market ### Tests Added comprehensive integration test suite in `tests/cancelTriggerOrdersOnPositionClose.ts`: - Test 1: Verifies trigger orders are cancelled when position fully closes - Test 2: Verifies trigger orders remain active on partial position close - Test 3: Verifies multiple trigger orders are all cancelled together ## Coverage - ✅ Perp markets (perpetual futures) - ✅ Spot markets - ✅ Multiple trigger orders per market - ✅ Partial vs full position closure handling ## Testing - Code compiles successfully with no errors - Follows existing code patterns and conventions - Proper error handling with DriftResult - Comprehensive inline documentation Author: Pranay Email: pranaygaurav4555@gmail.com GitHub: @pranay123-stack
1 parent c74d5e9 commit 8d9f930

File tree

2 files changed

+580
-0
lines changed

2 files changed

+580
-0
lines changed

programs/drift/src/controller/orders.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,89 @@ pub fn cancel_orders(
584584
Ok(canceled_order_ids)
585585
}
586586

587+
/// Cancels all trigger orders for a specific market when a position is completely closed.
588+
///
589+
/// This function is called after a position is fully closed to automatically cancel any
590+
/// remaining trigger orders (stop-loss/take-profit) for that market, preventing orphaned
591+
/// orders from executing unexpectedly and creating unintended positions.
592+
///
593+
/// # Arguments
594+
/// * `user` - Mutable reference to the user account
595+
/// * `user_key` - Public key of the user
596+
/// * `market_index` - Index of the market whose trigger orders should be cancelled
597+
/// * `market_type` - Type of market (Perp or Spot)
598+
/// * `perp_market_map` - Reference to perp market map
599+
/// * `spot_market_map` - Reference to spot market map
600+
/// * `oracle_map` - Mutable reference to oracle map
601+
/// * `now` - Current timestamp
602+
/// * `slot` - Current slot number
603+
///
604+
/// # Returns
605+
/// * `DriftResult` - Ok(()) if successful, error otherwise
606+
///
607+
/// # Example Flow
608+
/// 1. User opens short position with stop-loss trigger order at $110
609+
/// 2. User manually closes position at current price
610+
/// 3. This function cancels the stop-loss order to prevent it from triggering later
611+
///
612+
/// # See Also
613+
/// * Issue #923: https://github.com/drift-labs/protocol-v2/issues/923
614+
pub fn cancel_trigger_orders_for_closed_position(
615+
user: &mut User,
616+
user_key: &Pubkey,
617+
market_index: u16,
618+
market_type: MarketType,
619+
perp_market_map: &PerpMarketMap,
620+
spot_market_map: &SpotMarketMap,
621+
oracle_map: &mut OracleMap,
622+
now: i64,
623+
slot: u64,
624+
) -> DriftResult {
625+
// Iterate through all user orders to find trigger orders for this market
626+
for order_index in 0..user.orders.len() {
627+
// Skip orders that are not currently open
628+
if user.orders[order_index].status != OrderStatus::Open {
629+
continue;
630+
}
631+
632+
// Only cancel trigger orders (TriggerMarket or TriggerLimit)
633+
// Regular limit/market orders are not affected
634+
if !user.orders[order_index].must_be_triggered() {
635+
continue;
636+
}
637+
638+
// Only cancel orders for the specified market type (perp vs spot)
639+
if user.orders[order_index].market_type != market_type {
640+
continue;
641+
}
642+
643+
// Only cancel orders for the specific market index
644+
if user.orders[order_index].market_index != market_index {
645+
continue;
646+
}
647+
648+
// Cancel the trigger order
649+
cancel_order(
650+
order_index,
651+
user,
652+
user_key,
653+
perp_market_map,
654+
spot_market_map,
655+
oracle_map,
656+
now,
657+
slot,
658+
OrderActionExplanation::OrderExpired,
659+
None,
660+
0,
661+
false,
662+
)?;
663+
}
664+
665+
user.update_last_active_slot(slot);
666+
667+
Ok(())
668+
}
669+
587670
pub fn cancel_order_by_order_id(
588671
order_id: u32,
589672
user: &AccountLoader<User>,
@@ -2042,6 +2125,28 @@ fn fulfill_perp_order(
20422125
)?;
20432126
}
20442127

2128+
// Fix for issue #923: Cancel all trigger orders if position is completely closed
2129+
// Only check if we actually filled something (base_asset_amount > 0)
2130+
if base_asset_amount > 0 {
2131+
let position_index = get_position_index(&user.perp_positions, market_index)?;
2132+
2133+
// If position is now completely closed (base_asset_amount == 0), cancel all trigger orders
2134+
// This prevents orphaned stop-loss/take-profit orders from executing unexpectedly
2135+
if user.perp_positions[position_index].base_asset_amount == 0 {
2136+
cancel_trigger_orders_for_closed_position(
2137+
user,
2138+
user_key,
2139+
market_index,
2140+
MarketType::Perp,
2141+
perp_market_map,
2142+
spot_market_map,
2143+
oracle_map,
2144+
now,
2145+
slot,
2146+
)?;
2147+
}
2148+
}
2149+
20452150
Ok((base_asset_amount, quote_asset_amount))
20462151
}
20472152

@@ -4680,6 +4785,28 @@ fn fulfill_spot_order(
46804785
}
46814786
}
46824787

4788+
// Fix for issue #923: Cancel all trigger orders if spot position is completely closed
4789+
// Only check if we actually filled something (base_asset_amount > 0)
4790+
if base_asset_amount > 0 {
4791+
let spot_position_index = user.get_spot_position_index(base_market_index)?;
4792+
4793+
// If spot position is now completely closed (scaled_balance == 0), cancel all trigger orders
4794+
// This prevents orphaned stop-loss/take-profit orders from executing unexpectedly
4795+
if user.spot_positions[spot_position_index].scaled_balance == 0 {
4796+
cancel_trigger_orders_for_closed_position(
4797+
user,
4798+
user_key,
4799+
base_market_index,
4800+
MarketType::Spot,
4801+
perp_market_map,
4802+
spot_market_map,
4803+
oracle_map,
4804+
now,
4805+
slot,
4806+
)?;
4807+
}
4808+
}
4809+
46834810
Ok((base_asset_amount, quote_asset_amount))
46844811
}
46854812

0 commit comments

Comments
 (0)