Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b58cda0
init new margin calc
crispheaney Jul 20, 2025
820c232
deposit and transfer into
crispheaney Jul 23, 2025
9efd808
add settle pnl
crispheaney Jul 23, 2025
75b92f8
program: add withdraw
crispheaney Jul 24, 2025
162fc23
add more ix
crispheaney Jul 24, 2025
82463f3
add new meets withdraw req fn
crispheaney Jul 26, 2025
fb57e5f
enter/exit liquidation logic
crispheaney Jul 27, 2025
4de579a
moar
crispheaney Jul 27, 2025
085e805
start liquidation logic
crispheaney Jul 30, 2025
4e7db0f
other liquidation fns
crispheaney Jul 30, 2025
8e89ef4
make build work
crispheaney Jul 30, 2025
8062d60
more updates
crispheaney Jul 31, 2025
991dda9
always calc isolated pos
crispheaney Aug 4, 2025
c627c1e
rm isolated position market index logic
crispheaney Aug 4, 2025
a00f3a9
moar
crispheaney Aug 5, 2025
d435dad
program: rm the isolated position market index
crispheaney Aug 5, 2025
ed76b47
some tweaks
crispheaney Aug 5, 2025
c13a605
rm some old margin code
crispheaney Aug 5, 2025
4a9aadc
tweak meets withdraw requirements
crispheaney Aug 5, 2025
0d56488
rm liquidation mode changing context
crispheaney Aug 6, 2025
584337b
handle liquidation id and bit flags
crispheaney Aug 7, 2025
15c05ee
more liquidation changes
crispheaney Aug 7, 2025
adc2815
clean
crispheaney Aug 8, 2025
0de7802
fix force cancel orders
crispheaney Aug 8, 2025
830c7c9
update validate liquidation
crispheaney Aug 8, 2025
5d09739
moar
crispheaney Aug 8, 2025
7392d3e
rename is_being_liquidated
crispheaney Aug 8, 2025
26960c8
start adding test
crispheaney Aug 15, 2025
2ab06e3
program: add validate for liq borrow for perp pnl
crispheaney Aug 15, 2025
9a56326
program: add test for isolated margin calc
crispheaney Aug 15, 2025
b171c23
is bankrupt test
crispheaney Aug 15, 2025
2821269
fix cancel orders
crispheaney Aug 19, 2025
424987f
fix set liquidation status
crispheaney Aug 19, 2025
b84daf1
more tweaks
crispheaney Aug 19, 2025
ea09842
clean up naming
crispheaney Aug 19, 2025
cc397f0
update last active slot for isolated position liq
crispheaney Aug 20, 2025
9833303
another liquidation review
crispheaney Aug 21, 2025
9cb040a
add test
crispheaney Aug 21, 2025
8177496
cargo fmt --
crispheaney Aug 21, 2025
9a8ec1a
tweak naming
crispheaney Aug 21, 2025
6ddbaf8
add test to make sure false liquidaiton wont be triggered
crispheaney Aug 25, 2025
2db2907
test meets withdraw
crispheaney Aug 25, 2025
8314bbe
change is bankrupt
crispheaney Aug 26, 2025
654683c
more
crispheaney Aug 26, 2025
4cab732
update uses of exit isolated liquidaiton
crispheaney Aug 26, 2025
6a6a150
moar
crispheaney Aug 26, 2025
7c46187
moar
crispheaney Aug 26, 2025
51ae2eb
reduce diff
crispheaney Aug 26, 2025
bae1b6b
moar
crispheaney Aug 26, 2025
d2f08ea
modularize some for tests
crispheaney Aug 26, 2025
ba8866a
add tests for the pnl for deposit liquidation
crispheaney Aug 27, 2025
9fa04fa
tests for isolated position transfer
crispheaney Aug 27, 2025
a732348
test for update spot balance
crispheaney Aug 28, 2025
91baee3
test for settle pnl
crispheaney Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 124 additions & 102 deletions programs/drift/src/controller/liquidation.rs

Large diffs are not rendered by default.

64 changes: 54 additions & 10 deletions programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub fn place_perp_order(
spot_market_map,
oracle_map,
state.liquidation_margin_buffer_ratio,
Some(params.market_index),
)?;
}

Expand Down Expand Up @@ -356,14 +357,21 @@ pub fn place_perp_order(

options.update_risk_increasing(risk_increasing);

let isolated_position_market_index = if user.perp_positions[position_index].is_isolated() {
Some(market_index)
} else {
None
};

// when orders are placed in bulk, only need to check margin on last place
if options.enforce_margin_check && !options.is_liquidation() {
if (options.enforce_margin_check || isolated_position_market_index.is_some()) && !options.is_liquidation() {
meets_place_order_margin_requirement(
user,
perp_market_map,
spot_market_map,
oracle_map,
options.risk_increasing,
isolated_position_market_index,
)?;
}

Expand Down Expand Up @@ -519,8 +527,10 @@ pub fn cancel_orders(
market_type: Option<MarketType>,
market_index: Option<u16>,
direction: Option<PositionDirection>,
skip_isolated_positions: bool,
) -> DriftResult<Vec<u32>> {
let mut canceled_order_ids: Vec<u32> = vec![];
let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::<Vec<u16>>();
for order_index in 0..user.orders.len() {
if user.orders[order_index].status != OrderStatus::Open {
continue;
Expand All @@ -534,6 +544,8 @@ pub fn cancel_orders(
if user.orders[order_index].market_index != market_index {
continue;
}
} else if skip_isolated_positions && isolated_position_market_indexes.contains(&user.orders[order_index].market_index) {
continue;
}

if let Some(direction) = direction {
Expand Down Expand Up @@ -1036,6 +1048,7 @@ pub fn fill_perp_order(
spot_market_map,
oracle_map,
state.liquidation_margin_buffer_ratio,
Some(market_index),
) {
Ok(_) => {}
Err(_) => {
Expand Down Expand Up @@ -1726,6 +1739,8 @@ fn fulfill_perp_order(
let user_order_position_decreasing =
determine_if_user_order_is_position_decreasing(user, market_index, user_order_index)?;

let user_is_isolated_position = user.get_perp_position(market_index)?.is_isolated();

let perp_market = perp_market_map.get_ref(&market_index)?;
let limit_price = fill_mode.get_limit_price(
&user.orders[user_order_index],
Expand Down Expand Up @@ -1761,7 +1776,7 @@ fn fulfill_perp_order(

let mut base_asset_amount = 0_u64;
let mut quote_asset_amount = 0_u64;
let mut maker_fills: BTreeMap<Pubkey, i64> = BTreeMap::new();
let mut maker_fills: BTreeMap<Pubkey, (i64, bool)> = BTreeMap::new();
let maker_direction = user.orders[user_order_index].direction.opposite();
for fulfillment_method in fulfillment_methods.iter() {
if user.orders[user_order_index].status != OrderStatus::Open {
Expand Down Expand Up @@ -1838,6 +1853,8 @@ fn fulfill_perp_order(
Some(&maker),
)?;

let maker_is_isolated_position = maker.get_perp_position(market_index)?.is_isolated();

let (fill_base_asset_amount, fill_quote_asset_amount, maker_fill_base_asset_amount) =
fulfill_perp_order_with_match(
market.deref_mut(),
Expand Down Expand Up @@ -1871,6 +1888,7 @@ fn fulfill_perp_order(
maker_key,
maker_direction,
maker_fill_base_asset_amount,
maker_is_isolated_position,
)?;
}

Expand All @@ -1893,7 +1911,7 @@ fn fulfill_perp_order(
quote_asset_amount
)?;

let total_maker_fill = maker_fills.values().sum::<i64>();
let total_maker_fill = maker_fills.values().map(|(fill, _)| fill).sum::<i64>();

validate!(
total_maker_fill.unsigned_abs() <= base_asset_amount,
Expand Down Expand Up @@ -1923,6 +1941,10 @@ fn fulfill_perp_order(
context = context.margin_ratio_override(MARGIN_PRECISION);
}

if user_is_isolated_position {
context = context.isolated_position_market_index(market_index);
}

let taker_margin_calculation =
calculate_margin_requirement_and_total_collateral_and_liability_info(
user,
Expand Down Expand Up @@ -1950,7 +1972,7 @@ fn fulfill_perp_order(
}
}

for (maker_key, maker_base_asset_amount_filled) in maker_fills {
for (maker_key, (maker_base_asset_amount_filled, maker_is_isolated_position)) in maker_fills {
let mut maker = makers_and_referrer.get_ref_mut(&maker_key)?;

let maker_stats = if maker.authority == user.authority {
Expand Down Expand Up @@ -1981,6 +2003,10 @@ fn fulfill_perp_order(
}
}

if maker_is_isolated_position {
context = context.isolated_position_market_index(market_index);
}

let maker_margin_calculation =
calculate_margin_requirement_and_total_collateral_and_liability_info(
&maker,
Expand Down Expand Up @@ -2049,20 +2075,21 @@ fn get_referrer<'a>(

#[inline(always)]
fn update_maker_fills_map(
map: &mut BTreeMap<Pubkey, i64>,
map: &mut BTreeMap<Pubkey, (i64, bool)>,
maker_key: &Pubkey,
maker_direction: PositionDirection,
fill: u64,
is_isolated_position: bool,
) -> DriftResult {
let signed_fill = match maker_direction {
PositionDirection::Long => fill.cast::<i64>()?,
PositionDirection::Short => -fill.cast::<i64>()?,
};

if let Some(maker_filled) = map.get_mut(maker_key) {
if let Some((maker_filled, _)) = map.get_mut(maker_key) {
*maker_filled = maker_filled.safe_add(signed_fill)?;
} else {
map.insert(*maker_key, signed_fill);
map.insert(*maker_key, (signed_fill, is_isolated_position));
}

Ok(())
Expand Down Expand Up @@ -2947,6 +2974,7 @@ pub fn trigger_order(
spot_market_map,
oracle_map,
state.liquidation_margin_buffer_ratio,
Some(market_index),
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
Expand Down Expand Up @@ -3072,8 +3100,14 @@ pub fn trigger_order(

// If order increases risk and user is below initial margin, cancel it
if is_risk_increasing && !user.orders[order_index].reduce_only {
let isolated_position_market_index = if user.get_perp_position(market_index)?.is_isolated() {
Some(market_index)
} else {
None
};

let meets_initial_margin_requirement =
meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?;
meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?;

if !meets_initial_margin_requirement {
cancel_order(
Expand Down Expand Up @@ -3224,6 +3258,11 @@ pub fn force_cancel_orders(
continue;
}

// TODO: handle force deleting these orders
if user.get_perp_position(market_index)?.is_isolated() {
continue;
}

state.perp_fee_structure.flat_filler_fee
}
};
Expand Down Expand Up @@ -3359,6 +3398,7 @@ pub fn place_spot_order(
spot_market_map,
oracle_map,
state.liquidation_margin_buffer_ratio,
None,
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
Expand Down Expand Up @@ -3571,6 +3611,7 @@ pub fn place_spot_order(
spot_market_map,
oracle_map,
options.risk_increasing,
None,
)?;
}

Expand Down Expand Up @@ -3702,6 +3743,7 @@ pub fn fill_spot_order(
spot_market_map,
oracle_map,
state.liquidation_margin_buffer_ratio,
None,
) {
Ok(_) => {}
Err(_) => {
Expand Down Expand Up @@ -4218,7 +4260,7 @@ fn fulfill_spot_order(

let mut base_asset_amount = 0_u64;
let mut quote_asset_amount = 0_u64;
let mut maker_fills: BTreeMap<Pubkey, i64> = BTreeMap::new();
let mut maker_fills: BTreeMap<Pubkey, (i64, bool)> = BTreeMap::new();
let maker_direction = user.orders[user_order_index].direction.opposite();
for fulfillment_method in fulfillment_methods.iter() {
if user.orders[user_order_index].status != OrderStatus::Open {
Expand Down Expand Up @@ -4260,6 +4302,7 @@ fn fulfill_spot_order(
maker_key,
maker_direction,
base_filled,
false,
)?;
}

Expand Down Expand Up @@ -5182,6 +5225,7 @@ pub fn trigger_spot_order(
spot_market_map,
oracle_map,
state.liquidation_margin_buffer_ratio,
None,
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
Expand Down Expand Up @@ -5331,7 +5375,7 @@ pub fn trigger_spot_order(
// If order is risk increasing and user is below initial margin, cancel it
if is_risk_increasing && !user.orders[order_index].reduce_only {
let meets_initial_margin_requirement =
meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?;
meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, None)?;

if !meets_initial_margin_requirement {
cancel_order(
Expand Down
67 changes: 51 additions & 16 deletions programs/drift/src/controller/pnl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ use crate::math::oracle::{is_oracle_valid_for_action, DriftAction};

use crate::math::casting::Cast;
use crate::math::margin::{
calculate_margin_requirement_and_total_collateral_and_liability_info,
meets_maintenance_margin_requirement, meets_settle_pnl_maintenance_margin_requirement,
MarginRequirementType,
};
use crate::math::position::calculate_base_asset_value_with_expiry_price;
use crate::math::safe_math::SafeMath;
Expand Down Expand Up @@ -83,14 +81,18 @@ pub fn settle_pnl(

// cannot settle negative pnl this way on a user who is in liquidation territory
if unrealized_pnl < 0 {
let isolated_position_market_index = user.perp_positions[position_index].is_isolated().then_some(market_index);

// may already be cached
let meets_margin_requirement = match meets_margin_requirement {
Some(meets_margin_requirement) => meets_margin_requirement,
None => meets_settle_pnl_maintenance_margin_requirement(
Some(meets_margin_requirement) if !isolated_position_market_index.is_some() => meets_margin_requirement,
// TODO check margin for isolate position
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to redo margin check?

_ => meets_settle_pnl_maintenance_margin_requirement(
user,
perp_market_map,
spot_market_map,
oracle_map,
isolated_position_market_index,
)?,
};

Expand Down Expand Up @@ -268,17 +270,43 @@ pub fn settle_pnl(
);
}

update_spot_balances(
pnl_to_settle_with_user.unsigned_abs(),
if pnl_to_settle_with_user > 0 {
&SpotBalanceType::Deposit
} else {
&SpotBalanceType::Borrow
},
spot_market,
user.get_quote_spot_position_mut(),
false,
)?;
if user.perp_positions[position_index].is_isolated() {
let perp_position = &mut user.perp_positions[position_index];
if pnl_to_settle_with_user < 0 {
let token_amount = perp_position.get_isolated_position_token_amount(spot_market)?;

validate!(
token_amount >= pnl_to_settle_with_user.unsigned_abs(),
ErrorCode::InsufficientCollateralForSettlingPNL,
"user has insufficient deposit for market {}",
market_index
)?;
}

update_spot_balances(
pnl_to_settle_with_user.unsigned_abs(),
if pnl_to_settle_with_user > 0 {
&SpotBalanceType::Deposit
} else {
&SpotBalanceType::Borrow
},
spot_market,
perp_position,
false,
)?;
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need to do this for settle_expired_position too? or will it settle to the user's spot pos without a problem?

update_spot_balances(
pnl_to_settle_with_user.unsigned_abs(),
if pnl_to_settle_with_user > 0 {
&SpotBalanceType::Deposit
} else {
&SpotBalanceType::Borrow
},
spot_market,
user.get_quote_spot_position_mut(),
false,
)?;
}

update_quote_asset_amount(
&mut user.perp_positions[position_index],
Expand Down Expand Up @@ -324,8 +352,14 @@ pub fn settle_expired_position(
) -> DriftResult {
validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;

let isolated_position_market_index = if user.get_perp_position(perp_market_index)?.is_isolated() {
Some(perp_market_index)
} else {
None
};

// cannot settle pnl this way on a user who is in liquidation territory
if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?)
if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?)
{
return Err(ErrorCode::InsufficientCollateralForSettlingPNL);
}
Expand Down Expand Up @@ -359,6 +393,7 @@ pub fn settle_expired_position(
Some(MarketType::Perp),
Some(perp_market_index),
None,
true,
)?;

let position_index = match get_position_index(&user.perp_positions, perp_market_index) {
Expand Down
3 changes: 2 additions & 1 deletion programs/drift/src/controller/pnl/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ pub fn user_does_not_meet_strict_maintenance_requirement() {
assert_eq!(result, Err(ErrorCode::InsufficientCollateralForSettlingPNL));

let meets_maintenance =
meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map)
meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map, None)
.unwrap();

assert_eq!(meets_maintenance, true);
Expand All @@ -410,6 +410,7 @@ pub fn user_does_not_meet_strict_maintenance_requirement() {
&market_map,
&spot_market_map,
&mut oracle_map,
None,
)
.unwrap();

Expand Down
6 changes: 4 additions & 2 deletions programs/drift/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ pub enum ErrorCode {
SpotMarketInsufficientDeposits,
#[msg("UserMustSettleTheirOwnPositiveUnsettledPNL")]
UserMustSettleTheirOwnPositiveUnsettledPNL,
#[msg("CantUpdatePoolBalanceType")]
CantUpdatePoolBalanceType,
#[msg("CantUpdateSpotBalanceType")]
CantUpdateSpotBalanceType,
#[msg("InsufficientCollateralForSettlingPNL")]
InsufficientCollateralForSettlingPNL,
#[msg("AMMNotUpdatedInSameSlot")]
Expand Down Expand Up @@ -639,6 +639,8 @@ pub enum ErrorCode {
InvalidIfRebalanceConfig,
#[msg("Invalid If Rebalance Swap")]
InvalidIfRebalanceSwap,
#[msg("Invalid Isolated Perp Market")]
InvalidIsolatedPerpMarket,
}

#[macro_export]
Expand Down
Loading