Skip to content
Open
Show file tree
Hide file tree
Changes from 27 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
417 changes: 224 additions & 193 deletions programs/drift/src/controller/liquidation.rs

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions programs/drift/src/controller/liquidation/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2197,7 +2197,7 @@ pub mod liquidate_perp {
.unwrap();

let market_after = perp_market_map.get_ref(&0).unwrap();
assert!(!user.is_being_liquidated());
assert!(!user.is_cross_margin_being_liquidated());
assert_eq!(market_after.amm.total_liquidation_fee, 41787043);
}

Expand Down Expand Up @@ -2351,7 +2351,7 @@ pub mod liquidate_perp {
.unwrap();

// user out of liq territory
assert!(!user.is_being_liquidated());
assert!(!user.is_cross_margin_being_liquidated());

let oracle_price = oracle_map
.get_price_data(&(oracle_price_key, OracleSource::Pyth))
Expand Down Expand Up @@ -4256,7 +4256,7 @@ pub mod liquidate_spot {
.unwrap();

assert_eq!(user.last_active_slot, 1);
assert_eq!(user.is_being_liquidated(), true);
assert_eq!(user.is_cross_margin_being_liquidated(), true);
assert_eq!(user.liquidation_margin_freed, 7000031);
assert_eq!(user.spot_positions[0].scaled_balance, 990558159000);
assert_eq!(user.spot_positions[1].scaled_balance, 9406768999);
Expand Down Expand Up @@ -4326,7 +4326,7 @@ pub mod liquidate_spot {
let pct_margin_freed = (user.liquidation_margin_freed as u128) * PRICE_PRECISION
/ (margin_shortage + user.liquidation_margin_freed as u128);
assert_eq!(pct_margin_freed, 433267); // ~43.3%
assert_eq!(user.is_being_liquidated(), true);
assert_eq!(user.is_cross_margin_being_liquidated(), true);

let slot = 136_u64;
liquidate_spot(
Expand All @@ -4353,7 +4353,7 @@ pub mod liquidate_spot {
assert_eq!(user.liquidation_margin_freed, 0);
assert_eq!(user.spot_positions[0].scaled_balance, 455580082000);
assert_eq!(user.spot_positions[1].scaled_balance, 4067681997);
assert_eq!(user.is_being_liquidated(), false);
assert_eq!(user.is_cross_margin_being_liquidated(), false);
}

#[test]
Expand Down Expand Up @@ -6873,7 +6873,7 @@ pub mod liquidate_perp_pnl_for_deposit {
)
.unwrap();

let margin_shortage = calc.margin_shortage().unwrap();
let margin_shortage = calc.cross_margin_margin_shortage().unwrap();

let pct_margin_freed = (user.liquidation_margin_freed as u128) * PRICE_PRECISION
/ (margin_shortage + user.liquidation_margin_freed as u128);
Expand Down Expand Up @@ -6914,7 +6914,7 @@ pub mod liquidate_perp_pnl_for_deposit {
)
.unwrap();

let margin_shortage = calc.margin_shortage().unwrap();
let margin_shortage = calc.cross_margin_margin_shortage().unwrap();

let pct_margin_freed = (user.liquidation_margin_freed as u128) * PRICE_PRECISION
/ (margin_shortage + user.liquidation_margin_freed as u128);
Expand Down Expand Up @@ -8560,7 +8560,7 @@ pub mod liquidate_spot_with_swap {
)
.unwrap();

assert_eq!(user.is_being_liquidated(), false);
assert_eq!(user.is_cross_margin_being_liquidated(), false);

let quote_spot_market = spot_market_map.get_ref(&0).unwrap();
let sol_spot_market = spot_market_map.get_ref(&1).unwrap();
Expand Down
63 changes: 49 additions & 14 deletions programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub fn place_perp_order(
)?;
}

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;

if params.is_update_high_leverage_mode() {
if let Some(config) = high_leverage_mode_config {
Expand Down Expand Up @@ -519,8 +519,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 +536,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 @@ -1024,7 +1028,7 @@ pub fn fill_perp_order(
"Order must be triggered first"
)?;

if user.is_bankrupt() {
if user.is_cross_margin_bankrupt() {
msg!("user is bankrupt");
return Ok((0, 0));
}
Expand Down Expand Up @@ -1475,7 +1479,7 @@ fn get_maker_orders_info(

let mut maker = load_mut!(user_account_loader)?;

if maker.is_being_liquidated() || maker.is_bankrupt() {
if maker.is_being_liquidated() {
continue;
}

Expand Down Expand Up @@ -1941,16 +1945,23 @@ fn fulfill_perp_order(
)?;

if !taker_margin_calculation.meets_margin_requirement() {
let (margin_requirement, total_collateral) = if taker_margin_calculation.has_isolated_position_margin_calculation(market_index) {
let isolated_position_margin_calculation = taker_margin_calculation.get_isolated_position_margin_calculation(market_index)?;
(isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral)
} else {
(taker_margin_calculation.margin_requirement, taker_margin_calculation.total_collateral)
};

msg!(
"taker breached fill requirements (margin requirement {}) (total_collateral {})",
taker_margin_calculation.margin_requirement,
taker_margin_calculation.total_collateral
margin_requirement,
total_collateral
);
return Err(ErrorCode::InsufficientCollateral);
}
}

for (maker_key, maker_base_asset_amount_filled) in maker_fills {
for (maker_key, (maker_base_asset_amount_filled)) 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 @@ -2001,11 +2012,18 @@ fn fulfill_perp_order(
}

if !maker_margin_calculation.meets_margin_requirement() {
let (margin_requirement, total_collateral) = if maker_margin_calculation.has_isolated_position_margin_calculation(market_index) {
let isolated_position_margin_calculation = maker_margin_calculation.get_isolated_position_margin_calculation(market_index)?;
(isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral)
} else {
(maker_margin_calculation.margin_requirement, maker_margin_calculation.total_collateral)
};

msg!(
"maker ({}) breached fill requirements (margin requirement {}) (total_collateral {})",
maker_key,
maker_margin_calculation.margin_requirement,
maker_margin_calculation.total_collateral
margin_requirement,
total_collateral
);
return Err(ErrorCode::InsufficientCollateral);
}
Expand Down Expand Up @@ -2949,7 +2967,7 @@ pub fn trigger_order(
state.liquidation_margin_buffer_ratio,
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;

let mut perp_market = perp_market_map.get_ref_mut(&market_index)?;
let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity(
Expand Down Expand Up @@ -3167,7 +3185,7 @@ pub fn force_cancel_orders(
ErrorCode::UserIsBeingLiquidated
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;

let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info(
user,
Expand All @@ -3184,6 +3202,8 @@ pub fn force_cancel_orders(
ErrorCode::SufficientCollateral
)?;

let cross_margin_meets_initial_margin_requirement = margin_calc.cross_margin_meets_margin_requirement();

let mut total_fee = 0_u64;

for order_index in 0..user.orders.len() {
Expand All @@ -3210,6 +3230,10 @@ pub fn force_cancel_orders(
continue;
}

if cross_margin_meets_initial_margin_requirement {
continue;
}

state.spot_fee_structure.flat_filler_fee
}
MarketType::Perp => {
Expand All @@ -3224,6 +3248,17 @@ pub fn force_cancel_orders(
continue;
}

if !user.get_perp_position(market_index)?.is_isolated() {
if cross_margin_meets_initial_margin_requirement {
continue;
}
} else {
let isolated_position_meets_margin_requirement = margin_calc.isolated_position_meets_margin_requirement(market_index)?;
if isolated_position_meets_margin_requirement {
continue;
}
}

state.perp_fee_structure.flat_filler_fee
}
};
Expand Down Expand Up @@ -3361,7 +3396,7 @@ pub fn place_spot_order(
state.liquidation_margin_buffer_ratio,
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;

if options.try_expire_orders {
expire_orders(
Expand Down Expand Up @@ -3691,7 +3726,7 @@ pub fn fill_spot_order(
"Order must be triggered first"
)?;

if user.is_bankrupt() {
if user.is_cross_margin_bankrupt() {
msg!("User is bankrupt");
return Ok(0);
}
Expand Down Expand Up @@ -3999,7 +4034,7 @@ fn get_spot_maker_orders_info(

let mut maker = load_mut!(user_account_loader)?;

if maker.is_being_liquidated() || maker.is_bankrupt() {
if maker.is_being_liquidated() {
continue;
}

Expand Down Expand Up @@ -5184,7 +5219,7 @@ pub fn trigger_spot_order(
state.liquidation_margin_buffer_ratio,
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;

let spot_market = spot_market_map.get_ref(&market_index)?;
let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity(
Expand Down
55 changes: 40 additions & 15 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 @@ -61,7 +59,7 @@ pub fn settle_pnl(
meets_margin_requirement: Option<bool>,
mode: SettlePnlMode,
) -> DriftResult {
validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;
let now = clock.unix_timestamp;
{
let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?;
Expand Down Expand Up @@ -268,17 +266,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 @@ -322,7 +346,7 @@ pub fn settle_expired_position(
clock: &Clock,
state: &State,
) -> DriftResult {
validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;
validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?;

// 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)?)
Expand Down Expand Up @@ -359,6 +383,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
Loading