From b58cda0037f22098bc6c7c84e7d336e8c7c311aa Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 20 Jul 2025 18:16:59 -0400 Subject: [PATCH 01/31] init new margin calc --- programs/drift/src/math/margin.rs | 173 +++++++++++++++++- .../drift/src/state/margin_calculation.rs | 8 + programs/drift/src/state/perp_market.rs | 2 +- programs/drift/src/state/user.rs | 49 ++++- 4 files changed, 220 insertions(+), 12 deletions(-) diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index cc6af96add..2901be1b1c 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -28,6 +28,8 @@ use crate::state::user::{MarketType, OrderFillSimulation, PerpPosition, User}; use num_integer::Roots; use std::cmp::{max, min, Ordering}; +use super::spot_balance::get_token_amount; + #[cfg(test)] mod tests; @@ -147,8 +149,8 @@ pub fn calculate_perp_position_value_and_pnl( }; // add small margin requirement for every open order - margin_requirement = margin_requirement - .safe_add(market_position.margin_requirement_for_open_orders()?)?; + margin_requirement = + margin_requirement.safe_add(market_position.margin_requirement_for_open_orders()?)?; let unrealized_asset_weight = market.get_unrealized_asset_weight(total_unrealized_pnl, margin_requirement_type)?; @@ -233,6 +235,10 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_map: &mut OracleMap, context: MarginContext, ) -> DriftResult { + if context.isolated_position_market_index.is_some() { + return calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position(user, perp_market_map, spot_market_map, oracle_map, context); + } + let mut calculation = MarginCalculation::new(context); let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { @@ -494,6 +500,10 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( continue; } + if market_position.is_isolated_position() { + continue; + } + let market = &perp_market_map.get_ref(&market_position.market_index)?; validate!( @@ -634,6 +644,165 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( Ok(calculation) } +pub fn calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position( + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + context: MarginContext, +) -> DriftResult { + let mut calculation = MarginCalculation::new(context); + + let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { + user.max_margin_ratio + } else { + 0_u32 + }; + + if let Some(margin_ratio_override) = context.margin_ratio_override { + user_custom_margin_ratio = margin_ratio_override.max(user_custom_margin_ratio); + } + + let user_pool_id = user.pool_id; + let user_high_leverage_mode = user.is_high_leverage_mode(); + + let isolated_position_market_index = context.isolated_position_market_index.unwrap(); + + let perp_position = user.get_perp_position(isolated_position_market_index)?; + + let perp_market = perp_market_map.get_ref(&isolated_position_market_index)?; + + validate!( + user_pool_id == perp_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) == perp market pool id ({})", + user_pool_id, + perp_market.pool_id, + )?; + + let quote_spot_market = spot_market_map.get_ref(&perp_market.quote_spot_market_index)?; + + validate!( + user_pool_id == quote_spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) == quote spot market pool id ({})", + user_pool_id, + quote_spot_market.pool_id, + )?; + + let (quote_oracle_price_data, quote_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + quote_spot_market.market_index, + "e_spot_market.oracle_id(), + quote_spot_market + .historical_oracle_data + .last_oracle_price_twap, + quote_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + let quote_oracle_valid = + is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; + + let quote_strict_oracle_price = StrictOraclePrice::new( + quote_oracle_price_data.price, + quote_spot_market + .historical_oracle_data + .last_oracle_price_twap_5min, + calculation.context.strict, + ); + quote_strict_oracle_price.validate()?; + + let quote_token_amount = get_token_amount( + perp_position + .isolated_position_scaled_balance + .cast::()?, + "e_spot_market, + &SpotBalanceType::Deposit, + )?; + + let quote_token_value = get_strict_token_value( + quote_token_amount.cast::()?, + quote_spot_market.decimals, + "e_strict_oracle_price, + )?; + + calculation.add_total_collateral(quote_token_value)?; + + calculation.update_all_deposit_oracles_valid(quote_oracle_valid); + + #[cfg(feature = "drift-rs")] + calculation.add_spot_asset_value(quote_token_value)?; + + let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Perp, + isolated_position_market_index, + &perp_market.oracle_id(), + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + perp_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + let ( + perp_margin_requirement, + weighted_pnl, + worst_case_liability_value, + open_order_margin_requirement, + base_asset_value, + ) = calculate_perp_position_value_and_pnl( + &perp_position, + &perp_market, + oracle_price_data, + "e_strict_oracle_price, + context.margin_type, + user_custom_margin_ratio, + user_high_leverage_mode, + calculation.track_open_orders_fraction(), + )?; + + calculation.add_margin_requirement( + perp_margin_requirement, + worst_case_liability_value, + MarketIdentifier::perp(isolated_position_market_index), + )?; + + calculation.add_total_collateral(weighted_pnl)?; + + #[cfg(feature = "drift-rs")] + calculation.add_perp_liability_value(worst_case_liability_value)?; + #[cfg(feature = "drift-rs")] + calculation.add_perp_pnl(weighted_pnl)?; + + let has_perp_liability = perp_position.base_asset_amount != 0 + || perp_position.quote_asset_amount < 0 + || perp_position.has_open_order(); + + if has_perp_liability { + calculation.add_perp_liability()?; + calculation.update_with_perp_isolated_liability( + perp_market.contract_tier == ContractTier::Isolated, + ); + } + + if has_perp_liability || calculation.context.margin_type != MarginRequirementType::Initial { + calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( + quote_oracle_validity, + Some(DriftAction::MarginCalc), + )?); + calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( + oracle_validity, + Some(DriftAction::MarginCalc), + )?); + } + + calculation.validate_num_spot_liabilities()?; + + Ok(calculation) +} + pub fn validate_any_isolated_tier_requirements( user: &User, calculation: MarginCalculation, diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4a0c299e4e..1756a5a7b8 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -35,6 +35,7 @@ pub struct MarginContext { pub fuel_perp_delta: Option<(u16, i64)>, pub fuel_spot_deltas: [(u16, i128); 2], pub margin_ratio_override: Option, + pub isolated_position_market_index: Option, } #[derive(PartialEq, Eq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] @@ -74,6 +75,7 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, + isolated_position_market_index: None, } } @@ -152,6 +154,7 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, + isolated_position_market_index: None, } } @@ -173,6 +176,11 @@ impl MarginContext { } Ok(self) } + + pub fn isolated_position_market_index(mut self, market_index: u16) -> Self { + self.isolated_position_market_index = Some(market_index); + self + } } #[derive(Clone, Copy, Debug)] diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 1adf030388..f33904c741 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -809,7 +809,7 @@ impl SpotBalance for PoolBalance { } fn update_balance_type(&mut self, _balance_type: SpotBalanceType) -> DriftResult { - Err(ErrorCode::CantUpdatePoolBalanceType) + Err(ErrorCode::CantUpdateSpotBalanceType) } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b32292bdbc..a31b7cc03d 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -11,8 +11,7 @@ use crate::math::orders::{ apply_protected_maker_limit_price_offset, standardize_base_asset_amount, standardize_price, }; use crate::math::position::{ - calculate_base_asset_value_and_pnl_with_oracle_price, - calculate_perp_liability_value, + calculate_base_asset_value_and_pnl_with_oracle_price, calculate_perp_liability_value, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{ @@ -21,7 +20,7 @@ use crate::math::spot_balance::{ use crate::math::stats::calculate_rolling_sum; use crate::msg; use crate::state::oracle::StrictOraclePrice; -use crate::state::perp_market::{ContractType}; +use crate::state::perp_market::ContractType; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::Size; use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; @@ -951,8 +950,8 @@ pub struct PerpPosition { pub lp_shares: u64, /// The last base asset amount per lp the amm had /// Used to settle the users lp position - /// precision: BASE_PRECISION - pub last_base_asset_amount_per_lp: i64, + /// precision: SPOT_BALANCE_PRECISION + pub isolated_position_scaled_balance: u64, /// The last quote asset amount per lp the amm had /// Used to settle the users lp position /// precision: QUOTE_PRECISION @@ -965,7 +964,7 @@ pub struct PerpPosition { pub market_index: u16, /// The number of open orders pub open_orders: u8, - pub per_lp_base: i8, + pub position_type: u8, } impl PerpPosition { @@ -974,9 +973,7 @@ impl PerpPosition { } pub fn is_available(&self) -> bool { - !self.is_open_position() - && !self.has_open_order() - && !self.has_unsettled_pnl() + !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() } pub fn is_open_position(&self) -> bool { @@ -1120,6 +1117,40 @@ impl PerpPosition { None } } + + pub fn is_isolated_position(&self) -> bool { + self.position_type == 1 + } +} + +impl SpotBalance for PerpPosition { + fn market_index(&self) -> u16 { + QUOTE_SPOT_MARKET_INDEX + } + + fn balance_type(&self) -> &SpotBalanceType { + &SpotBalanceType::Deposit + } + + fn balance(&self) -> u128 { + self.isolated_position_scaled_balance as u128 + } + + fn increase_balance(&mut self, delta: u128) -> DriftResult { + self.isolated_position_scaled_balance = + self.isolated_position_scaled_balance.safe_add(delta.cast::()?)?; + Ok(()) + } + + fn decrease_balance(&mut self, delta: u128) -> DriftResult { + self.isolated_position_scaled_balance = + self.isolated_position_scaled_balance.safe_sub(delta.cast::()?)?; + Ok(()) + } + + fn update_balance_type(&mut self, _balance_type: SpotBalanceType) -> DriftResult { + Err(ErrorCode::CantUpdateSpotBalanceType) + } } pub(crate) type PerpPositions = [PerpPosition; 8]; From 820c2322c606cec509e36b843736f893f8264a2b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 23 Jul 2025 19:29:15 -0400 Subject: [PATCH 02/31] deposit and transfer into --- programs/drift/src/error.rs | 6 +- programs/drift/src/instructions/user.rs | 347 ++++++++++++++++++++++++ programs/drift/src/math/margin.rs | 2 +- programs/drift/src/state/user.rs | 25 +- 4 files changed, 376 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 61dee9f5f8..acaa63947a 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -193,8 +193,8 @@ pub enum ErrorCode { SpotMarketInsufficientDeposits, #[msg("UserMustSettleTheirOwnPositiveUnsettledPNL")] UserMustSettleTheirOwnPositiveUnsettledPNL, - #[msg("CantUpdatePoolBalanceType")] - CantUpdatePoolBalanceType, + #[msg("CantUpdateSpotBalanceType")] + CantUpdateSpotBalanceType, #[msg("InsufficientCollateralForSettlingPNL")] InsufficientCollateralForSettlingPNL, #[msg("AMMNotUpdatedInSameSlot")] @@ -639,6 +639,8 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid Isolated Perp Market")] + InvalidIsolatedPerpMarket, } #[macro_export] diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 80c3d7c013..36046801d5 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -16,6 +16,7 @@ use crate::controller::orders::{cancel_orders, ModifyOrderId}; use crate::controller::position::update_position_and_market; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_revenue_pool_balances; +use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits, update_spot_balances_and_cumulative_deposits_with_limits, @@ -1896,6 +1897,300 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( Ok(()) } +#[access_control( + deposit_not_paused(&ctx.accounts.state) +)] +pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> Result<()> { + let user_key = ctx.accounts.user.key(); + let user = &mut load_mut!(ctx.accounts.user)?; + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let slot = clock.slot; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let mint = get_token_mint(remaining_accounts_iter)?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + let perp_market = perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; + + validate!( + user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + validate!( + !matches!(spot_market.status, MarketStatus::Initialized), + ErrorCode::MarketBeingInitialized, + "Market is being initialized" + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_price_data), + now, + )?; + + user.increment_total_deposits( + amount, + oracle_price_data.price, + spot_market.get_precision().cast()?, + )?; + + let total_deposits_after = user.total_deposits; + let total_withdraws_after = user.total_withdraws; + + let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + + update_spot_balances( + amount.cast::()?, + &SpotBalanceType::Deposit, + &mut spot_market, + perp_position, + false, + )?; + + validate!( + matches!(spot_market.status, MarketStatus::Active), + ErrorCode::MarketActionPaused, + "spot_market not active", + )?; + + drop(spot_market); + // TODO add back + // if user.is_being_liquidated() { + // // try to update liquidation status if user is was already being liq'd + // let is_being_liquidated = is_user_being_liquidated( + // user, + // &perp_market_map, + // &spot_market_map, + // &mut oracle_map, + // state.liquidation_margin_buffer_ratio, + // )?; + + // if !is_being_liquidated { + // user.exit_liquidation(); + // } + // } + + user.update_last_active_slot(slot); + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.spot_market_vault, + &ctx.accounts.authority, + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + ctx.accounts.spot_market_vault.reload()?; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let oracle_price = oracle_price_data.price; + + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Deposit, + amount, + oracle_price, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after, + total_withdraws_after, + market_index: spot_market_index, + explanation: DepositExplanation::None, + transfer_user: None, + }; + emit!(deposit_record); + + spot_market.validate_max_token_deposits_and_borrows(false)?; + + Ok(()) +} + +#[access_control( + deposit_not_paused(&ctx.accounts.state) + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferDepositIntoIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> anchor_lang::Result<()> { + let authority_key = ctx.accounts.authority.key; + let user_key = ctx.accounts.user.key(); + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let slot = clock.slot; + + let user = &mut load_mut!(ctx.accounts.user)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt" + )?; + + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + { + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + clock.unix_timestamp, + )?; + } + + { + let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + } + + { + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + } + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + )?; + + validate_spot_margin_trading( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; + + if user.is_being_liquidated() { + user.exit_liquidation(); + } + + user.update_last_active_slot(slot); + + { + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + validate!( + user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + spot_market, + perp_position, + false, + )?; + } + + let spot_market = spot_market_map.get_ref(&spot_market_index)?; + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + Ok(()) +} + + #[access_control( exchange_not_paused(&ctx.accounts.state) )] @@ -4330,6 +4625,58 @@ pub struct CancelOrder<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction(spot_market_index: u16,)] +pub struct DepositPerpPosition<'info> { + pub state: Box>, + #[account( + mut, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + constraint = is_stats_for_user(&user, &user_stats)? + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub authority: Signer<'info>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + constraint = &spot_market_vault.mint.eq(&user_token_account.mint), + token::authority = authority + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction(spot_market_index: u16,)] +pub struct TransferDepositIntoIsolatedPerpPosition<'info> { + #[account( + mut, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub authority: Signer<'info>, + pub state: Box>, + #[account( + seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, +} + #[derive(Accounts)] pub struct PlaceAndTake<'info> { pub state: Box>, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 2901be1b1c..126851057a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -500,7 +500,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( continue; } - if market_position.is_isolated_position() { + if market_position.is_isolated() { continue; } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index a31b7cc03d..42eafe858e 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -257,6 +257,29 @@ impl User { Ok(&mut self.perp_positions[position_index]) } + pub fn force_get_isolated_perp_position_mut( + &mut self, + perp_market_index: u16, + ) -> DriftResult<&mut PerpPosition> { + if let Ok(position_index) = get_position_index(&self.perp_positions, perp_market_index) { + let perp_position = &mut self.perp_positions[position_index]; + validate!( + perp_position.is_isolated(), + ErrorCode::InvalidPerpPosition, + "perp position is not isolated" + )?; + + Ok(&mut self.perp_positions[position_index]) + } else { + let position_index = add_new_position(&mut self.perp_positions, perp_market_index)?; + + let perp_position = &mut self.perp_positions[position_index]; + perp_position.position_type = 1; + + Ok(&mut self.perp_positions[position_index]) + } + } + pub fn get_order_index(&self, order_id: u32) -> DriftResult { self.orders .iter() @@ -1118,7 +1141,7 @@ impl PerpPosition { } } - pub fn is_isolated_position(&self) -> bool { + pub fn is_isolated(&self) -> bool { self.position_type == 1 } } From 9efd808c0e34a6a7f340b0d49aec754315ae65ad Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 23 Jul 2025 19:40:39 -0400 Subject: [PATCH 03/31] add settle pnl --- programs/drift/src/controller/pnl.rs | 53 +++++++++++++++++++++------- programs/drift/src/state/user.rs | 4 +++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 46cf1e9779..bd4427e9fd 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -85,8 +85,9 @@ pub fn settle_pnl( if unrealized_pnl < 0 { // 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 !user.perp_positions[position_index].is_isolated() => meets_margin_requirement, + // TODO check margin for isolate position + _ => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, @@ -268,17 +269,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 { + 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], diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 42eafe858e..2a474de4c5 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1144,6 +1144,10 @@ impl PerpPosition { pub fn is_isolated(&self) -> bool { self.position_type == 1 } + + pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount(self.isolated_position_scaled_balance as u128, spot_market, &SpotBalanceType::Deposit) + } } impl SpotBalance for PerpPosition { From 75b92f89a220009d3155796209427025798a2be1 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 24 Jul 2025 09:28:41 -0400 Subject: [PATCH 04/31] program: add withdraw --- programs/drift/src/instructions/user.rs | 187 ++++++++++++++++++++++++ programs/drift/src/state/user.rs | 6 + 2 files changed, 193 insertions(+) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 36046801d5..a17e38edf4 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -786,6 +786,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( amount as u128, &mut user_stats, now, + None, )?; validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; @@ -960,6 +961,7 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( amount as u128, user_stats, now, + None, )?; validate_spot_margin_trading( @@ -2144,6 +2146,7 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( amount as u128, user_stats, now, + None, )?; validate_spot_margin_trading( @@ -2190,6 +2193,156 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( Ok(()) } +#[access_control( + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, Withdraw<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> anchor_lang::Result<()> { + let user_key = ctx.accounts.user.key(); + let user = &mut load_mut!(ctx.accounts.user)?; + let mut user_stats = load_mut!(ctx.accounts.user_stats)?; + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let slot = clock.slot; + let state = &ctx.accounts.state; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &get_writable_spot_market_set(spot_market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let mint = get_token_mint(remaining_accounts_iter)?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + { + let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + now, + )?; + + user.increment_total_withdraws( + amount, + oracle_price_data.price, + spot_market.get_precision().cast()?, + )?; + + let isolated_perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + + let isolated_position_token_amount = isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + + validate!( + amount as u128 <= isolated_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + isolated_perp_position, + true, + )?; + } + + // this is wrong + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + &mut user_stats, + now, + Some(perp_market_index), + )?; + + // TODO figure out what to do here + // if user.is_being_liquidated() { + // user.exit_liquidation(); + // } + + user.update_last_active_slot(slot); + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price = oracle_map.get_price_data(&spot_market.oracle_id())?.price; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Withdraw, + oracle_price, + amount, + market_index: spot_market_index, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after: user.total_deposits, + total_withdraws_after: user.total_withdraws, + explanation: DepositExplanation::None, + transfer_user: None, + }; + emit!(deposit_record); + + controller::token::send_from_program_vault( + &ctx.accounts.token_program, + &ctx.accounts.spot_market_vault, + &ctx.accounts.user_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + // reload the spot market vault balance so it's up-to-date + ctx.accounts.spot_market_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + spot_market.validate_max_token_deposits_and_borrows(false)?; + + Ok(()) +} #[access_control( exchange_not_paused(&ctx.accounts.state) @@ -4677,6 +4830,40 @@ pub struct TransferDepositIntoIsolatedPerpPosition<'info> { pub spot_market_vault: Box>, } +#[derive(Accounts)] +#[instruction(spot_market_index: u16)] +pub struct WithdrawFromIsolatedPerpPosition<'info> { + pub state: Box>, + #[account( + mut, + has_one = authority, + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub authority: Signer<'info>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + constraint = state.signer.eq(&drift_signer.key()) + )] + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + #[account( + mut, + constraint = &spot_market_vault.mint.eq(&user_token_account.mint) + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, +} + #[derive(Accounts)] pub struct PlaceAndTake<'info> { pub state: Box>, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 2a474de4c5..c38d81291f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -587,6 +587,7 @@ impl User { withdraw_amount: u128, user_stats: &mut UserStats, now: i64, + isolated_perp_position_market_index: Option, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; let context = MarginContext::standard(margin_requirement_type) @@ -595,6 +596,11 @@ impl User { .fuel_spot_delta(withdraw_market_index, withdraw_amount.cast::()?) .fuel_numerator(self, now); + // TODO check if this is correct + if let Some(isolated_perp_position_market_index) = isolated_perp_position_market_index { + context.isolated_position_market_index(isolated_perp_position_market_index); + } + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, perp_market_map, From 162fc23d08c9a6464996233f6be622849d7d4460 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 24 Jul 2025 09:48:46 -0400 Subject: [PATCH 05/31] add more ix --- programs/drift/src/instructions/user.rs | 152 +++++++++++++++--------- programs/drift/src/lib.rs | 27 +++++ 2 files changed, 125 insertions(+), 54 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a17e38edf4..fbb883d72b 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1903,7 +1903,7 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( deposit_not_paused(&ctx.accounts.state) )] pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, DepositPerpPosition<'info>>, + ctx: Context<'_, '_, 'c, 'info, DepositIsolatedPerpPosition<'info>>, spot_market_index: u16, perp_market_index: u16, amount: u64, @@ -2064,11 +2064,11 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( deposit_not_paused(&ctx.accounts.state) withdraw_not_paused(&ctx.accounts.state) )] -pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, TransferDepositIntoIsolatedPerpPosition<'info>>, +pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferIsolatedPerpPositionDeposit<'info>>, spot_market_index: u16, perp_market_index: u16, - amount: u64, + amount: i64, ) -> anchor_lang::Result<()> { let authority_key = ctx.accounts.authority.key; let user_key = ctx.accounts.user.key(); @@ -2101,18 +2101,9 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - { - let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; - let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; - controller::spot_balance::update_spot_market_cumulative_interest( - spot_market, - Some(oracle_price_data), - clock.unix_timestamp, - )?; - } - { let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; validate!( perp_market.quote_spot_market_index == spot_market_index, @@ -2121,69 +2112,122 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( perp_market.quote_spot_market_index, spot_market_index )?; + + validate!( + user.pool_id == spot_market.pool_id && user.pool_id == perp_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + clock.unix_timestamp, + )?; } - { - let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + if amount > 0 { + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; update_spot_balances_and_cumulative_deposits( amount as u128, &SpotBalanceType::Borrow, - spot_market, + &mut spot_market, &mut user.spot_positions[spot_position_index], false, None, )?; - } - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( - &perp_market_map, - &spot_market_map, - &mut oracle_map, - MarginRequirementType::Initial, - spot_market_index, - amount as u128, - user_stats, - now, - None, - )?; + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; - validate_spot_margin_trading( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + drop(spot_market); - if user.is_being_liquidated() { - user.exit_liquidation(); - } + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + None, + )?; - user.update_last_active_slot(slot); + validate_spot_margin_trading( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; - { - let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + if user.is_being_liquidated() { + user.exit_liquidation(); + } + } else { + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + + let isolated_perp_position_token_amount = user.force_get_isolated_perp_position_mut(perp_market_index)?.get_isolated_position_token_amount(&spot_market)?; validate!( - user.pool_id == spot_market.pool_id, - ErrorCode::InvalidPoolId, - "user pool id ({}) != market pool id ({})", - user.pool_id, - spot_market.pool_id + amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index )?; - let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; update_spot_balances( amount as u128, - &SpotBalanceType::Deposit, - spot_market, - perp_position, + &SpotBalanceType::Borrow, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, false, )?; + + drop(spot_market); + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + Some(perp_market_index), + )?; + + // TODO figure out what to do here + // if user.is_being_liquidated() { + // user.exit_liquidation(); + // } } + + + user.update_last_active_slot(slot); + let spot_market = spot_market_map.get_ref(&spot_market_index)?; math::spot_withdraw::validate_spot_market_vault_amount( &spot_market, @@ -2197,7 +2241,7 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( withdraw_not_paused(&ctx.accounts.state) )] pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, Withdraw<'info>>, + ctx: Context<'_, '_, 'c, 'info, WithdrawIsolatedPerpPosition<'info>>, spot_market_index: u16, perp_market_index: u16, amount: u64, @@ -4780,7 +4824,7 @@ pub struct CancelOrder<'info> { #[derive(Accounts)] #[instruction(spot_market_index: u16,)] -pub struct DepositPerpPosition<'info> { +pub struct DepositIsolatedPerpPosition<'info> { pub state: Box>, #[account( mut, @@ -4810,7 +4854,7 @@ pub struct DepositPerpPosition<'info> { #[derive(Accounts)] #[instruction(spot_market_index: u16,)] -pub struct TransferDepositIntoIsolatedPerpPosition<'info> { +pub struct TransferIsolatedPerpPositionDeposit<'info> { #[account( mut, constraint = can_sign_for_user(&user, &authority)? @@ -4832,7 +4876,7 @@ pub struct TransferDepositIntoIsolatedPerpPosition<'info> { #[derive(Accounts)] #[instruction(spot_market_index: u16)] -pub struct WithdrawFromIsolatedPerpPosition<'info> { +pub struct WithdrawIsolatedPerpPosition<'info> { pub state: Box>, #[account( mut, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index cedcbfbfeb..5f172e3043 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -168,6 +168,33 @@ pub mod drift { handle_transfer_perp_position(ctx, market_index, amount) } + pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, + ) -> Result<()> { + handle_deposit_into_isolated_perp_position(ctx, spot_market_index, perp_market_index, amount) + } + + pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferIsolatedPerpPositionDeposit<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: i64, + ) -> Result<()> { + handle_transfer_isolated_perp_position_deposit(ctx, spot_market_index, perp_market_index, amount) + } + + pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, + ) -> Result<()> { + handle_withdraw_from_isolated_perp_position(ctx, spot_market_index, perp_market_index, amount) + } + pub fn place_perp_order<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, PlaceOrder>, params: OrderParams, From 82463f3b351f4f54af35fd0169bf2ac01f2efd11 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 26 Jul 2025 18:09:16 -0400 Subject: [PATCH 06/31] add new meets withdraw req fn --- programs/drift/src/instructions/user.rs | 6 ++-- programs/drift/src/state/user.rs | 42 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index fbb883d72b..eb4f5bbe60 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2206,16 +2206,14 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( drop(spot_market); - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + user.meets_withdraw_margin_requirement_for_isolated_perp_position( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - spot_market_index, - amount as u128, user_stats, now, - Some(perp_market_index), + perp_market_index, )?; // TODO figure out what to do here diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index c38d81291f..0bb2d212e1 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -638,6 +638,48 @@ impl User { Ok(true) } + pub fn meets_withdraw_margin_requirement_for_isolated_perp_position( + &mut self, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + margin_requirement_type: MarginRequirementType, + user_stats: &mut UserStats, + now: i64, + isolated_perp_position_market_index: u16, + ) -> DriftResult { + let strict = margin_requirement_type == MarginRequirementType::Initial; + let context = MarginContext::standard(margin_requirement_type) + .strict(strict) + .isolated_position_market_index(isolated_perp_position_market_index); + + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + self, + perp_market_map, + spot_market_map, + oracle_map, + context, + )?; + + if calculation.margin_requirement > 0 || calculation.get_num_of_liabilities()? > 0 { + validate!( + calculation.all_liability_oracles_valid, + ErrorCode::InvalidOracle, + "User attempting to withdraw with outstanding liabilities when an oracle is invalid" + )?; + } + + validate!( + calculation.meets_margin_requirement(), + ErrorCode::InsufficientCollateral, + "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", + calculation.total_collateral, + calculation.margin_requirement + )?; + + Ok(true) + } + pub fn can_skip_auction_duration(&self, user_stats: &UserStats) -> DriftResult { if user_stats.disable_update_perp_bid_ask_twap { return Ok(false); From fb57e5f0e44e73158254a66d05bd145c1a8fe096 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 26 Jul 2025 20:06:49 -0400 Subject: [PATCH 07/31] enter/exit liquidation logic --- programs/drift/src/instructions/user.rs | 102 +++++++++++++++--------- programs/drift/src/math/liquidation.rs | 21 +++++ programs/drift/src/state/user.rs | 46 ++++++++++- 3 files changed, 130 insertions(+), 39 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index eb4f5bbe60..0e9c8cae84 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -33,6 +33,7 @@ use crate::instructions::optional_accounts::{ }; use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; +use crate::math::liquidation::is_isolated_position_being_liquidated; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; @@ -1980,15 +1981,17 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( let total_deposits_after = user.total_deposits; let total_withdraws_after = user.total_withdraws; - let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + { + let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; - update_spot_balances( - amount.cast::()?, - &SpotBalanceType::Deposit, - &mut spot_market, - perp_position, - false, - )?; + update_spot_balances( + amount.cast::()?, + &SpotBalanceType::Deposit, + &mut spot_market, + perp_position, + false, + )?; + } validate!( matches!(spot_market.status, MarketStatus::Active), @@ -1997,21 +2000,22 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( )?; drop(spot_market); - // TODO add back - // if user.is_being_liquidated() { - // // try to update liquidation status if user is was already being liq'd - // let is_being_liquidated = is_user_being_liquidated( - // user, - // &perp_market_map, - // &spot_market_map, - // &mut oracle_map, - // state.liquidation_margin_buffer_ratio, - // )?; - - // if !is_being_liquidated { - // user.exit_liquidation(); - // } - // } + + if user.is_isolated_position_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_position_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_position_liquidation(perp_market_index)?; + } + } user.update_last_active_slot(slot); @@ -2174,6 +2178,22 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( if user.is_being_liquidated() { user.exit_liquidation(); } + + if user.is_isolated_position_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_position_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_position_liquidation(perp_market_index)?; + } + } } else { let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; @@ -2216,10 +2236,24 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( perp_market_index, )?; - // TODO figure out what to do here - // if user.is_being_liquidated() { - // user.exit_liquidation(); - // } + if user.is_isolated_position_being_liquidated(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } + + if user.is_being_liquidated() { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_user_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_liquidation(); + } + } } @@ -2315,23 +2349,19 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( )?; } - // this is wrong - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + user.meets_withdraw_margin_requirement_for_isolated_perp_position( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - spot_market_index, - amount as u128, &mut user_stats, now, - Some(perp_market_index), + perp_market_index, )?; - // TODO figure out what to do here - // if user.is_being_liquidated() { - // user.exit_liquidation(); - // } + if user.is_isolated_position_being_liquidated(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } user.update_last_active_slot(slot); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 24a54afc59..4035719290 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -246,6 +246,27 @@ pub fn validate_user_not_being_liquidated( Ok(()) } +pub fn is_isolated_position_being_liquidated( + user: &User, + market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + perp_market_index: u16, + liquidation_margin_buffer_ratio: u32, +) -> DriftResult { + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(perp_market_index), + )?; + + let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + + Ok(is_being_liquidated) +} + pub enum LiquidationMultiplierType { Discount, Premium, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 0bb2d212e1..ef5a2e7c55 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -274,12 +274,23 @@ impl User { let position_index = add_new_position(&mut self.perp_positions, perp_market_index)?; let perp_position = &mut self.perp_positions[position_index]; - perp_position.position_type = 1; + perp_position.position_flag = PositionFlag::IsolatedPosition as u8; Ok(&mut self.perp_positions[position_index]) } } + pub fn get_isolated_perp_position(&self, perp_market_index: u16) -> DriftResult<&PerpPosition> { + let position_index = get_position_index(&self.perp_positions, perp_market_index)?; + validate!( + self.perp_positions[position_index].is_isolated(), + ErrorCode::InvalidPerpPosition, + "perp position is not isolated" + )?; + + Ok(&self.perp_positions[position_index]) + } + pub fn get_order_index(&self, order_id: u32) -> DriftResult { self.orders .iter() @@ -386,6 +397,29 @@ impl User { self.liquidation_margin_freed = 0; } + pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + if self.is_isolated_position_being_liquidated(perp_market_index)? { + return self.next_liquidation_id.safe_sub(1); + } + + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + + perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; + + Ok(get_then_update_id!(self, next_liquidation_id)) + } + + pub fn exit_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::BeingLiquidated as u8); + Ok(()) + } + + pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { + let perp_position = self.get_isolated_perp_position(perp_market_index)?; + Ok(perp_position.position_flag & PositionFlag::BeingLiquidated as u8 != 0) + } + pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { self.liquidation_margin_freed = self.liquidation_margin_freed.safe_add(margin_free)?; Ok(()) @@ -1035,7 +1069,7 @@ pub struct PerpPosition { pub market_index: u16, /// The number of open orders pub open_orders: u8, - pub position_type: u8, + pub position_flag: u8, } impl PerpPosition { @@ -1190,7 +1224,7 @@ impl PerpPosition { } pub fn is_isolated(&self) -> bool { - self.position_type == 1 + self.position_flag & PositionFlag::IsolatedPosition as u8 == PositionFlag::IsolatedPosition as u8 } pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { @@ -1691,6 +1725,12 @@ pub enum OrderBitFlag { SafeTriggerOrder = 0b00000100, } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum PositionFlag { + IsolatedPosition = 0b00000001, + BeingLiquidated = 0b00000010, +} + #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] From 4de579a96ce812b747dc465e3dcecc651125d1c4 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 26 Jul 2025 20:40:01 -0400 Subject: [PATCH 08/31] moar --- programs/drift/src/controller/liquidation.rs | 6 +-- programs/drift/src/controller/orders.rs | 20 ++++++++-- programs/drift/src/controller/pnl.rs | 17 +++++++- programs/drift/src/controller/pnl/tests.rs | 3 +- programs/drift/src/instructions/keeper.rs | 27 +++++++++---- programs/drift/src/instructions/user.rs | 33 ++++++++++++---- programs/drift/src/math/margin.rs | 41 ++++++++++++++++++-- programs/drift/src/state/user.rs | 1 + 8 files changed, 121 insertions(+), 27 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 137bc1d055..f722025728 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -556,7 +556,7 @@ pub fn liquidate_perp( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; validate!( liquidator_meets_initial_margin_requirement, @@ -2706,7 +2706,7 @@ pub fn liquidate_borrow_for_perp_pnl( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; validate!( liquidator_meets_initial_margin_requirement, @@ -3207,7 +3207,7 @@ pub fn liquidate_perp_pnl_for_deposit( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; validate!( liquidator_meets_initial_margin_requirement, diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 4502129845..bc7cc76ad9 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -356,14 +356,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, )?; } @@ -3072,8 +3079,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( @@ -3571,6 +3584,7 @@ pub fn place_spot_order( spot_market_map, oracle_map, options.risk_increasing, + None, )?; } @@ -5331,7 +5345,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( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index bd4427e9fd..01dd0a55d4 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -83,15 +83,22 @@ 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 = if user.perp_positions[position_index].is_isolated() { + Some(market_index) + } else { + None + }; + // may already be cached let meets_margin_requirement = match meets_margin_requirement { - Some(meets_margin_requirement) if !user.perp_positions[position_index].is_isolated() => meets_margin_requirement, + Some(meets_margin_requirement) if !isolated_position_market_index.is_some() => meets_margin_requirement, // TODO check margin for isolate position _ => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, + isolated_position_market_index, )?, }; @@ -351,8 +358,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); } diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 4a35df4e49..ee6dd872b7 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -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); @@ -410,6 +410,7 @@ pub fn user_does_not_meet_strict_maintenance_requirement() { &market_map, &spot_market_map, &mut oracle_map, + None, ) .unwrap(); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 222ec30755..f62df6fb1d 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -989,12 +989,25 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + let mut try_cache_margin_requirement = false; + for market_index in market_indexes.iter() { + if !user.get_perp_position(*market_index)?.is_isolated() { + try_cache_margin_requirement = true; + break; + } + } + + let meets_margin_requirement = if try_cache_margin_requirement { + Some(meets_settle_pnl_maintenance_margin_requirement( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + None, + )?) + } else { + None + }; for market_index in market_indexes.iter() { let market_in_settlement = @@ -1035,7 +1048,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( &mut oracle_map, &clock, state, - Some(meets_margin_requirement), + meets_margin_requirement, mode, ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 0e9c8cae84..a8e6ace067 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -39,7 +39,7 @@ use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_l use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ calculate_max_withdrawable_amount, meets_maintenance_margin_requirement, - meets_place_order_margin_requirement, validate_spot_margin_trading, MarginRequirementType, + validate_spot_margin_trading, MarginRequirementType, }; use crate::math::oracle::is_oracle_valid_for_action; use crate::math::oracle::DriftAction; @@ -3515,7 +3515,7 @@ pub fn handle_update_user_pool_id<'c: 'info, 'info>( user.pool_id = pool_id; // will throw if user has deposits/positions in other pools - meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; + meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map, None)?; Ok(()) } @@ -3741,12 +3741,29 @@ pub fn handle_enable_user_high_leverage_mode<'c: 'info, 'info>( "user already in high leverage mode" )?; - meets_maintenance_margin_requirement( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + let has_non_isolated_position = user.perp_positions.iter().any(|position| !position.is_isolated()); + + if has_non_isolated_position { + meets_maintenance_margin_requirement( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + None, + )?; + } + + let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::>(); + + for market_index in isolated_position_market_indexes.iter() { + meets_maintenance_margin_requirement( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + Some(*market_index), + )?; + } let mut config = load_mut!(ctx.accounts.high_leverage_mode_config)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 126851057a..aa81af0add 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -228,6 +228,7 @@ pub fn calculate_user_safest_position_tiers( Ok((safest_tier_spot_liablity, safest_tier_perp_liablity)) } +// todo make sure everything using this sets isolated_position_market_index correctly pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( user: &User, perp_market_map: &PerpMarketMap, @@ -848,6 +849,7 @@ pub fn meets_place_order_margin_requirement( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, risk_increasing: bool, + isolated_position_market_index: Option, ) -> DriftResult { let margin_type = if risk_increasing { MarginRequirementType::Initial @@ -856,6 +858,10 @@ pub fn meets_place_order_margin_requirement( }; let context = MarginContext::standard(margin_type).strict(true); + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, @@ -884,13 +890,20 @@ pub fn meets_initial_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + isolated_position_market_index: Option, ) -> DriftResult { + let context = MarginContext::standard(MarginRequirementType::Initial); + + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Initial), + context, ) .map(|calc| calc.meets_margin_requirement()) } @@ -900,13 +913,20 @@ pub fn meets_settle_pnl_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + isolated_position_market_index: Option, ) -> DriftResult { + let context = MarginContext::standard(MarginRequirementType::Maintenance).strict(true); + + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance).strict(true), + context, ) .map(|calc| calc.meets_margin_requirement()) } @@ -916,13 +936,20 @@ pub fn meets_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + isolated_position_market_index: Option, ) -> DriftResult { + let context = MarginContext::standard(MarginRequirementType::Maintenance); + + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance), + context, ) .map(|calc| calc.meets_margin_requirement()) } @@ -1110,6 +1137,14 @@ pub fn calculate_user_equity( all_oracles_valid &= is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; + if market_position.is_isolated() { + let quote_token_amount = market_position.get_isolated_position_token_amount("e_spot_market)?; + + let token_value = get_token_value(quote_token_amount.cast()?, quote_spot_market.decimals, quote_oracle_price_data.price)?; + + net_usd_value = net_usd_value.safe_add(token_value)?; + } + quote_oracle_price_data.price }; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index ef5a2e7c55..b5a0d5cd76 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -398,6 +398,7 @@ impl User { } pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + // todo figure out liquidation id if self.is_isolated_position_being_liquidated(perp_market_index)? { return self.next_liquidation_id.safe_sub(1); } From 085e8057076c89eba7ff7efff4cf5569cbf998e2 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 30 Jul 2025 17:34:06 -0400 Subject: [PATCH 09/31] start liquidation logic --- programs/drift/src/controller/liquidation.rs | 53 ++++--- programs/drift/src/controller/orders.rs | 9 ++ programs/drift/src/controller/pnl.rs | 1 + programs/drift/src/instructions/keeper.rs | 7 + programs/drift/src/instructions/user.rs | 1 + programs/drift/src/math/bankruptcy.rs | 10 ++ programs/drift/src/state/liquidation_mode.rs | 153 +++++++++++++++++++ programs/drift/src/state/mod.rs | 1 + programs/drift/src/state/user.rs | 16 +- 9 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 programs/drift/src/state/liquidation_mode.rs diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index f722025728..390e4a8c8b 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; use crate::msg; +use crate::state::liquidation_mode::{get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -139,20 +140,22 @@ pub fn liquidate_perp( now, )?; + let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(user)?; + if !user_is_being_liquidated && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if user_is_being_liquidated && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -184,6 +187,7 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; + let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -194,9 +198,10 @@ pub fn liquidate_perp( now, slot, OrderActionExplanation::Liquidation, + cancel_order_market_type, + cancel_order_market_index, None, - None, - None, + cancel_order_skip_isolated_positions, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -219,19 +224,16 @@ pub fn liquidate_perp( drop(market); - // burning lp shares = removing open bids/asks - let lp_shares = 0; - // check if user exited liquidation territory - let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { + let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + margin_context, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -257,13 +259,13 @@ pub fn liquidate_perp( liquidate_perp: LiquidatePerpRecord { market_index, oracle_price, - lp_shares, + lp_shares: 0, ..LiquidatePerpRecord::default() }, ..LiquidationRecord::default() }); - user.exit_liquidation(); + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -328,6 +330,7 @@ pub fn liquidate_perp( .get_price_data("e_spot_market.oracle_id())? .price; + // todo how to handle slot not being on perp position? let liquidator_fee = get_liquidation_fee( market.get_base_liquidator_fee(user.is_high_leverage_mode()), market.get_max_liquidation_fee()?, @@ -365,7 +368,7 @@ pub fn liquidate_perp( drop(market); drop(quote_spot_market); - let max_pct_allowed = calculate_max_pct_to_liquidate( + let max_pct_allowed = liquidation_mode.calculate_max_pct_to_liquidate( user, margin_shortage, slot, @@ -545,18 +548,21 @@ pub fn liquidate_perp( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - user.increment_margin_freed(margin_freed_for_perp_position)?; + liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); if base_asset_amount >= base_asset_amount_to_cover_margin_shortage { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.is_user_bankrupt(user)? { + liquidation_mode.enter_bankruptcy(user); } + let liquidator_isolated_position_market_index = liquidator.get_perp_position(market_index)?.is_isolated().then_some(market_index); + let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; validate!( liquidator_meets_initial_margin_requirement, @@ -688,7 +694,7 @@ pub fn liquidate_perp( oracle_price, base_asset_amount: user_position_delta.base_asset_amount, quote_asset_amount: user_position_delta.quote_asset_amount, - lp_shares, + lp_shares: 0, user_order_id, liquidator_order_id, fill_record_id, @@ -3630,6 +3636,7 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, + margin_context: MarginContext, ) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( @@ -3637,7 +3644,7 @@ pub fn calculate_margin_freed( perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), + margin_context, )?; let new_margin_shortage = margin_calculation_after.margin_shortage()?; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index bc7cc76ad9..a1cab6290c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -526,8 +526,10 @@ pub fn cancel_orders( market_type: Option, market_index: Option, direction: Option, + skip_isolated_positions: bool, ) -> DriftResult> { let mut canceled_order_ids: Vec = vec![]; + let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::>(); for order_index in 0..user.orders.len() { if user.orders[order_index].status != OrderStatus::Open { continue; @@ -541,6 +543,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 { @@ -3237,6 +3241,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 } }; diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 01dd0a55d4..cff0b45ccb 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -399,6 +399,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) { diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index f62df6fb1d..a272f8e06e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2858,6 +2858,13 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( None, None, None, + false, + )?; + + validate!( + !user.perp_positions.iter().any(|p| !p.is_available()), + ErrorCode::DefaultError, + "user must have no perp positions" )?; for spot_position in user.spot_positions.iter_mut() { diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a8e6ace067..ec68ed4306 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2614,6 +2614,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, + true, )?; Ok(()) diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 287b103060..6e152857af 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -33,3 +33,13 @@ pub fn is_user_bankrupt(user: &User) -> bool { has_liability } + +pub fn is_user_isolated_position_bankrupt(user: &User, market_index: u16) -> DriftResult { + let perp_position = user.get_isolated_perp_position(market_index)?; + + if perp_position.isolated_position_scaled_balance > 0 { + return Ok(false); + } + + return Ok(perp_position.base_asset_amount == 0 && perp_position.quote_asset_amount < 0 && !perp_position.has_open_order()); +} \ No newline at end of file diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs new file mode 100644 index 0000000000..6648417692 --- /dev/null +++ b/programs/drift/src/state/liquidation_mode.rs @@ -0,0 +1,153 @@ +use crate::{error::DriftResult, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate}, state::margin_calculation::{MarginContext, MarketIdentifier}, LIQUIDATION_PCT_PRECISION}; + +use super::user::{MarketType, User}; + +pub trait LiquidatePerpMode { + fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult; + + fn user_is_being_liquidated(&self, user: &User) -> DriftResult; + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; + + fn get_cancel_orders_params(&self) -> (Option, Option, bool); + + fn calculate_max_pct_to_liquidate( + &self, + user: &User, + margin_shortage: u128, + slot: u64, + initial_pct_to_liquidate: u128, + liquidation_duration: u128, + ) -> DriftResult; + + fn increment_free_margin(&self, user: &mut User, amount: u64); + + fn is_user_bankrupt(&self, user: &User) -> DriftResult; + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; +} + +pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { + Box::new(CrossMarginLiquidatePerpMode::new(market_index)) +} + +pub struct CrossMarginLiquidatePerpMode { + pub market_index: u16, +} + +impl CrossMarginLiquidatePerpMode { + pub fn new(market_index: u16) -> Self { + Self { market_index } + } +} + +impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { + fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + } + + fn user_is_being_liquidated(&self, user: &User) -> DriftResult { + Ok(user.is_being_liquidated()) + } + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { + Ok(user.exit_liquidation()) + } + + fn get_cancel_orders_params(&self) -> (Option, Option, bool) { + (None, None, true) + } + + fn calculate_max_pct_to_liquidate( + &self, + user: &User, + margin_shortage: u128, + slot: u64, + initial_pct_to_liquidate: u128, + liquidation_duration: u128, + ) -> DriftResult { + calculate_max_pct_to_liquidate( + user, + margin_shortage, + slot, + initial_pct_to_liquidate, + liquidation_duration, + ) + } + + fn increment_free_margin(&self, user: &mut User, amount: u64) { + user.increment_margin_freed(amount); + } + + fn is_user_bankrupt(&self, user: &User) -> DriftResult { + Ok(is_user_bankrupt(user)) + } + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + Ok(user.enter_bankruptcy()) + } + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + Ok(user.exit_bankruptcy()) + } +} + +pub struct IsolatedLiquidatePerpMode { + pub market_index: u16, +} + +impl IsolatedLiquidatePerpMode { + pub fn new(market_index: u16) -> Self { + Self { market_index } + } +} + +impl LiquidatePerpMode for IsolatedLiquidatePerpMode { + fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .isolated_position_market_index(self.market_index) + .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + } + + fn user_is_being_liquidated(&self, user: &User) -> DriftResult { + user.is_isolated_position_being_liquidated(self.market_index) + } + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { + user.exit_isolated_position_liquidation(self.market_index) + } + + fn get_cancel_orders_params(&self) -> (Option, Option, bool) { + (Some(MarketType::Perp), Some(self.market_index), true) + } + + fn calculate_max_pct_to_liquidate( + &self, + user: &User, + margin_shortage: u128, + slot: u64, + initial_pct_to_liquidate: u128, + liquidation_duration: u128, + ) -> DriftResult { + Ok(LIQUIDATION_PCT_PRECISION) + } + + fn increment_free_margin(&self, user: &mut User, amount: u64) { + return; + } + + fn is_user_bankrupt(&self, user: &User) -> DriftResult { + is_user_isolated_position_bankrupt(user, self.market_index) + } + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + user.enter_isolated_position_bankruptcy(self.market_index) + } + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + user.exit_isolated_position_bankruptcy(self.market_index) + } +} \ No newline at end of file diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index a9c9724757..65fdacf16d 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -5,6 +5,7 @@ pub mod fulfillment_params; pub mod high_leverage_mode_config; pub mod if_rebalance_config; pub mod insurance_fund_stake; +pub mod liquidation_mode; pub mod load_ref; pub mod margin_calculation; pub mod oracle; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b5a0d5cd76..e0fc445204 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -418,7 +418,20 @@ impl User { pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.position_flag & PositionFlag::BeingLiquidated as u8 != 0) + Ok(perp_position.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0) + } + + pub fn enter_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::BeingLiquidated as u8); + perp_position.position_flag |= PositionFlag::Bankruptcy as u8; + Ok(()) + } + + pub fn exit_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + perp_position.position_flag &= !(PositionFlag::Bankruptcy as u8); + Ok(()) } pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { @@ -1730,6 +1743,7 @@ pub enum OrderBitFlag { pub enum PositionFlag { IsolatedPosition = 0b00000001, BeingLiquidated = 0b00000010, + Bankruptcy = 0b00000100, } #[account(zero_copy(unsafe))] From 4e7db0fac9c6b2a4510fa98a719f47de2a70dc6d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 30 Jul 2025 19:13:50 -0400 Subject: [PATCH 10/31] other liquidation fns --- programs/drift/src/controller/liquidation.rs | 152 +++++++++---------- programs/drift/src/state/liquidation_mode.rs | 145 +++++++++++++++++- programs/drift/src/state/user.rs | 5 + 3 files changed, 216 insertions(+), 86 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 390e4a8c8b..37cf9cd0f6 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -96,8 +96,10 @@ pub fn liquidate_perp( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; + let liquidation_mode = get_perp_liquidation_mode(user, market_index); + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -140,14 +142,12 @@ pub fn liquidate_perp( now, )?; - let liquidation_mode = get_perp_liquidation_mode(user, market_index); - let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(user)?; @@ -226,7 +226,7 @@ pub fn liquidate_perp( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -242,7 +242,7 @@ pub fn liquidate_perp( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - user.increment_margin_freed(margin_freed)?; + liquidation_mode.increment_free_margin(user, margin_freed); if intermediate_margin_calculation.can_exit_liquidation()? { emit!(LiquidationRecord { @@ -555,7 +555,7 @@ pub fn liquidate_perp( if base_asset_amount >= base_asset_amount_to_cover_margin_shortage { liquidation_mode.exit_liquidation(user)?; - } else if liquidation_mode.is_user_bankrupt(user)? { + } else if liquidation_mode.should_user_enter_bankruptcy(user)? { liquidation_mode.enter_bankruptcy(user); } @@ -733,8 +733,10 @@ pub fn liquidate_perp_with_fill( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; + let liquidation_mode = get_perp_liquidation_mode(user, market_index); + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -777,20 +779,20 @@ pub fn liquidate_perp_with_fill( now, )?; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + margin_context, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -811,7 +813,8 @@ pub fn liquidate_perp_with_fill( || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - + + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, user_key, @@ -822,9 +825,10 @@ pub fn liquidate_perp_with_fill( now, slot, OrderActionExplanation::Liquidation, + cancel_orders_market_type, + cancel_orders_market_index, None, - None, - None, + cancel_orders_is_isolated, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -847,19 +851,16 @@ pub fn liquidate_perp_with_fill( drop(market); - // burning lp shares = removing open bids/asks - let lp_shares = 0; - // check if user exited liquidation territory - let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { + let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + margin_context, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -868,7 +869,7 @@ pub fn liquidate_perp_with_fill( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - user.increment_margin_freed(margin_freed)?; + liquidation_mode.increment_free_margin(user, margin_freed); if intermediate_margin_calculation.can_exit_liquidation()? { emit!(LiquidationRecord { @@ -885,7 +886,7 @@ pub fn liquidate_perp_with_fill( liquidate_perp: LiquidatePerpRecord { market_index, oracle_price, - lp_shares, + lp_shares: 0, ..LiquidatePerpRecord::default() }, ..LiquidationRecord::default() @@ -963,7 +964,7 @@ pub fn liquidate_perp_with_fill( drop(market); drop(quote_spot_market); - let max_pct_allowed = calculate_max_pct_to_liquidate( + let max_pct_allowed = liquidation_mode.calculate_max_pct_to_liquidate( &user, margin_shortage, slot, @@ -1104,15 +1105,16 @@ pub fn liquidate_perp_with_fill( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - user.increment_margin_freed(margin_freed_for_perp_position)?; + liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); if margin_calculation_after.meets_margin_requirement() { - user.exit_liquidation(); - } else if is_user_bankrupt(&user) { - user.enter_bankruptcy(); + liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.enter_bankruptcy(user)?; } let user_position_delta = get_position_delta_for_fill( @@ -1137,7 +1139,7 @@ pub fn liquidate_perp_with_fill( oracle_price, base_asset_amount: user_position_delta.base_asset_amount, quote_asset_amount: user_position_delta.quote_asset_amount, - lp_shares, + lp_shares: 0, user_order_id: order_id, liquidator_order_id: 0, fill_record_id, @@ -1672,6 +1674,8 @@ pub fn liquidate_spot( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -2772,8 +2776,10 @@ pub fn liquidate_perp_pnl_for_deposit( // blocked when 1) user deposit oracle is deemed invalid // or 2) user has outstanding liability with higher tier + let liquidation_mode = get_perp_liquidation_mode(user, perp_market_index); + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -2821,13 +2827,7 @@ pub fn liquidate_perp_pnl_for_deposit( e })?; - user.get_spot_position(asset_market_index).map_err(|_| { - msg!( - "User does not have a spot balance for asset market {}", - asset_market_index - ); - ErrorCode::CouldNotFindSpotPosition - })?; + liquidation_mode.validate_spot_position(user, asset_market_index)?; liquidator .force_get_perp_position_mut(perp_market_index) @@ -2878,22 +2878,8 @@ pub fn liquidate_perp_pnl_for_deposit( )?; let token_price = asset_price_data.price; - let spot_position = user.get_spot_position(asset_market_index)?; - - validate!( - spot_position.balance_type == SpotBalanceType::Deposit, - ErrorCode::WrongSpotBalanceType, - "User did not have a deposit for the asset market" - )?; - let token_amount = spot_position.get_token_amount(&asset_market)?; - - validate!( - token_amount != 0, - ErrorCode::InvalidSpotPosition, - "asset token amount zero for market index = {}", - asset_market_index - )?; + let token_amount = liquidation_mode.get_spot_token_amount(user, &asset_market)?; ( token_amount, @@ -2955,25 +2941,27 @@ pub fn liquidate_perp_pnl_for_deposit( ) }; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), + margin_context, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { - user.exit_liquidation(); + } else if liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } let liquidation_id = user.enter_liquidation(slot)?; let mut margin_freed = 0_u64; + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -2984,25 +2972,27 @@ pub fn liquidate_perp_pnl_for_deposit( now, slot, OrderActionExplanation::Liquidation, + cancel_orders_market_type, + cancel_orders_market_index, None, - None, - None, + cancel_orders_is_isolated, )?; let (safest_tier_spot_liability, safest_tier_perp_liability) = - calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; + liquidation_mode.calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; let is_contract_tier_violation = !(contract_tier.is_as_safe_as(&safest_tier_perp_liability, &safest_tier_spot_liability)); // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), + margin_context, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -3011,7 +3001,7 @@ pub fn liquidate_perp_pnl_for_deposit( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - user.increment_margin_freed(margin_freed)?; + liquidation_mode.increment_free_margin(user, margin_freed); let exiting_liq_territory = intermediate_margin_calculation.can_exit_liquidation()?; @@ -3042,7 +3032,7 @@ pub fn liquidate_perp_pnl_for_deposit( }); if exiting_liq_territory { - user.exit_liquidation(); + liquidation_mode.exit_liquidation(user)?; } else if is_contract_tier_violation { msg!( "return early after cancel orders: liquidating contract tier={:?} pnl is riskier than outstanding {:?} & {:?}", @@ -3088,7 +3078,7 @@ pub fn liquidate_perp_pnl_for_deposit( 0, // no if fee )?; - let max_pct_allowed = calculate_max_pct_to_liquidate( + let max_pct_allowed = liquidation_mode.calculate_max_pct_to_liquidate( user, margin_shortage, slot, @@ -3176,12 +3166,10 @@ pub fn liquidate_perp_pnl_for_deposit( Some(asset_transfer), )?; - update_spot_balances_and_cumulative_deposits( + liquidation_mode.decrease_spot_token_amount( + user, asset_transfer, - &SpotBalanceType::Borrow, &mut asset_market, - user.get_spot_position_mut(asset_market_index)?, - false, Some(asset_transfer), )?; } @@ -3202,18 +3190,21 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; - user.increment_margin_freed(margin_freed_from_liability)?; + liquidation_mode.increment_free_margin(user, margin_freed_from_liability); if pnl_transfer >= pnl_transfer_to_cover_margin_shortage { - user.exit_liquidation(); - } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.enter_bankruptcy(user)?; } + let liquidator_isolated_position_market_index = liquidator.get_perp_position(perp_market_index)?.is_isolated().then_some(perp_market_index); + let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; validate!( liquidator_meets_initial_margin_requirement, @@ -3262,12 +3253,14 @@ pub fn resolve_perp_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - if !user.is_bankrupt() && is_user_bankrupt(user) { - user.enter_bankruptcy(); + let liquidation_mode = get_perp_liquidation_mode(user, market_index); + + if !liquidation_mode.user_is_bankrupt(user)? && liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.enter_bankruptcy(user)?; } validate!( - user.is_bankrupt(), + liquidation_mode.user_is_bankrupt(user)?, ErrorCode::UserNotBankrupt, "user not bankrupt", )?; @@ -3314,6 +3307,7 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let MarginCalculation { margin_requirement, total_collateral, @@ -3323,7 +3317,7 @@ pub fn resolve_perp_bankruptcy( perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance), + margin_context, )?; // spot market's insurance fund draw attempt here (before social loss) @@ -3446,8 +3440,8 @@ pub fn resolve_perp_bankruptcy( } // exit bankruptcy - if !is_user_bankrupt(user) { - user.exit_bankruptcy(); + if !liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.exit_bankruptcy(user)?; } let liquidation_id = user.next_liquidation_id.safe_sub(1)?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 6648417692..ed15ef0262 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -1,6 +1,8 @@ -use crate::{error::DriftResult, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate}, state::margin_calculation::{MarginContext, MarketIdentifier}, LIQUIDATION_PCT_PRECISION}; +use solana_program::msg; -use super::user::{MarketType, User}; +use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers}, state::margin_calculation::{MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; + +use super::{perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult; @@ -24,9 +26,25 @@ pub trait LiquidatePerpMode { fn is_user_bankrupt(&self, user: &User) -> DriftResult; + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult; + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()>; fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()>; + + fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult; + + fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)>; + + fn decrease_spot_token_amount( + &self, + user: &mut User, + token_amount: u128, + spot_market: &mut SpotMarket, + cumulative_deposit_delta: Option, + ) -> DriftResult<()>; } pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { @@ -45,8 +63,7 @@ impl CrossMarginLiquidatePerpMode { impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) } fn user_is_being_liquidated(&self, user: &User) -> DriftResult { @@ -86,6 +103,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(is_user_bankrupt(user)) } + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { + Ok(is_user_bankrupt(user)) + } + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { Ok(user.enter_bankruptcy()) } @@ -93,6 +114,65 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { Ok(user.exit_bankruptcy()) } + + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { + if user.get_spot_position(asset_market_index).is_err() { + msg!( + "User does not have a spot balance for asset market {}", + asset_market_index + ); + + return Err(ErrorCode::CouldNotFindSpotPosition); + } + + Ok(()) + } + + fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult { + let spot_position = user.get_spot_position(spot_market.market_index)?; + + validate!( + spot_position.balance_type == SpotBalanceType::Deposit, + ErrorCode::WrongSpotBalanceType, + "User did not have a deposit for the asset market" + )?; + + let token_amount = spot_position.get_token_amount(&spot_market)?; + + validate!( + token_amount != 0, + ErrorCode::InvalidSpotPosition, + "asset token amount zero for market index = {}", + spot_market.market_index + )?; + + Ok(token_amount) + } + + fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)> { + calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map) + } + + fn decrease_spot_token_amount( + &self, + user: &mut User, + token_amount: u128, + spot_market: &mut SpotMarket, + cumulative_deposit_delta: Option, + ) -> DriftResult<()> { + let spot_position = user.get_spot_position_mut(spot_market.market_index)?; + + update_spot_balances_and_cumulative_deposits( + token_amount, + &SpotBalanceType::Borrow, + spot_market, + spot_position, + false, + cumulative_deposit_delta, + )?; + + Ok(()) + } } pub struct IsolatedLiquidatePerpMode { @@ -107,9 +187,7 @@ impl IsolatedLiquidatePerpMode { impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .isolated_position_market_index(self.market_index) - .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(self.market_index)) } fn user_is_being_liquidated(&self, user: &User) -> DriftResult { @@ -140,6 +218,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { } fn is_user_bankrupt(&self, user: &User) -> DriftResult { + user.is_isolated_position_bankrupt(self.market_index) + } + + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { is_user_isolated_position_bankrupt(user, self.market_index) } @@ -150,4 +232,53 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { user.exit_isolated_position_bankruptcy(self.market_index) } + + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { + validate!( + asset_market_index == QUOTE_SPOT_MARKET_INDEX, + ErrorCode::CouldNotFindSpotPosition, + "asset market index must be quote asset market index for isolated liquidation mode" + ) + } + + fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult { + let isolated_perp_position = user.get_isolated_perp_position(self.market_index)?; + + let token_amount = isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + + validate!( + token_amount != 0, + ErrorCode::InvalidSpotPosition, + "asset token amount zero for market index = {}", + spot_market.market_index + )?; + + Ok(token_amount) + } + + fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)> { + let contract_tier = perp_market_map.get_ref(&self.market_index)?.contract_tier; + + Ok((AssetTier::default(), contract_tier)) + } + + fn decrease_spot_token_amount( + &self, + user: &mut User, + token_amount: u128, + spot_market: &mut SpotMarket, + cumulative_deposit_delta: Option, + ) -> DriftResult<()> { + let perp_position = user.get_isolated_perp_position_mut(&self.market_index)?; + + update_spot_balances( + token_amount, + &SpotBalanceType::Borrow, + spot_market, + perp_position, + false, + )?; + + Ok(()) + } } \ No newline at end of file diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index e0fc445204..f32c7dd9f7 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -434,6 +434,11 @@ impl User { Ok(()) } + pub fn is_isolated_position_bankrupt(&self, perp_market_index: u16) -> DriftResult { + let perp_position = self.get_isolated_perp_position(perp_market_index)?; + Ok(perp_position.position_flag & (PositionFlag::Bankruptcy as u8) != 0) + } + pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { self.liquidation_margin_freed = self.liquidation_margin_freed.safe_add(margin_free)?; Ok(()) From 8e89ef411f4efe2ace98cd141c8ee7caf25a2ef3 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 30 Jul 2025 19:32:19 -0400 Subject: [PATCH 11/31] make build work --- programs/drift/src/controller/liquidation.rs | 71 +++++++++++++------- programs/drift/src/math/bankruptcy.rs | 1 + programs/drift/src/state/liquidation_mode.rs | 2 +- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 37cf9cd0f6..e4bec91158 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -96,10 +96,10 @@ pub fn liquidate_perp( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; - let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index); validate!( - !liquidation_mode.is_user_bankrupt(user)?, + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -150,7 +150,7 @@ pub fn liquidate_perp( liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(user)?; + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; if !user_is_being_liquidated && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); @@ -733,10 +733,10 @@ pub fn liquidate_perp_with_fill( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; - let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index); validate!( - !liquidation_mode.is_user_bankrupt(user)?, + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -788,11 +788,11 @@ pub fn liquidate_perp_with_fill( margin_context, )?; - if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { - liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -869,7 +869,7 @@ pub fn liquidate_perp_with_fill( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - liquidation_mode.increment_free_margin(user, margin_freed); + liquidation_mode.increment_free_margin(&mut user, margin_freed); if intermediate_margin_calculation.can_exit_liquidation()? { emit!(LiquidationRecord { @@ -1109,12 +1109,12 @@ pub fn liquidate_perp_with_fill( )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); + liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position); if margin_calculation_after.meets_margin_requirement() { - liquidation_mode.exit_liquidation(user)?; - } else if liquidation_mode.should_user_enter_bankruptcy(user)? { - liquidation_mode.enter_bankruptcy(user)?; + liquidation_mode.exit_liquidation(&mut user)?; + } else if liquidation_mode.should_user_enter_bankruptcy(&user)? { + liquidation_mode.enter_bankruptcy(&mut user)?; } let user_position_delta = get_position_delta_for_fill( @@ -1410,6 +1410,7 @@ pub fn liquidate_spot( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1940,6 +1941,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, + true )?; // check if user exited liquidation territory @@ -2246,6 +2248,7 @@ pub fn liquidate_spot_with_swap_end( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + MarginContext::liquidation(liquidation_margin_buffer_ratio) )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2512,6 +2515,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, + true )?; // check if user exited liquidation territory @@ -2705,6 +2709,8 @@ pub fn liquidate_borrow_for_perp_pnl( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -2776,10 +2782,10 @@ pub fn liquidate_perp_pnl_for_deposit( // blocked when 1) user deposit oracle is deemed invalid // or 2) user has outstanding liability with higher tier - let liquidation_mode = get_perp_liquidation_mode(user, perp_market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, perp_market_index); validate!( - !liquidation_mode.is_user_bankrupt(user)?, + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -2950,10 +2956,10 @@ pub fn liquidate_perp_pnl_for_deposit( margin_context, )?; - if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { + } else if liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -3253,14 +3259,14 @@ pub fn resolve_perp_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index); - if !liquidation_mode.user_is_bankrupt(user)? && liquidation_mode.should_user_enter_bankruptcy(user)? { + if !liquidation_mode.is_user_bankrupt(&user)? && liquidation_mode.should_user_enter_bankruptcy(&user)? { liquidation_mode.enter_bankruptcy(user)?; } validate!( - liquidation_mode.user_is_bankrupt(user)?, + liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserNotBankrupt, "user not bankrupt", )?; @@ -3307,7 +3313,7 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; + let margin_context = liquidation_mode.get_margin_context(0)?; let MarginCalculation { margin_requirement, total_collateral, @@ -3679,11 +3685,26 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { - msg!("margin calculation: {:?}", margin_calculation); - return Err(ErrorCode::SufficientCollateral); - } else { + if !user.is_being_liquidated() && !margin_calculation.meets_margin_requirement() { user.enter_liquidation(slot)?; } + + let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); + + for market_index in isolated_position_market_indexes { + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), + )?; + + if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { + user.enter_isolated_position_liquidation(market_index)?; + } + + } + Ok(()) } diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 6e152857af..7defdea6b3 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -1,3 +1,4 @@ +use crate::error::DriftResult; use crate::state::spot_market::SpotBalanceType; use crate::state::user::User; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index ed15ef0262..a86d5a929f 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -269,7 +269,7 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { spot_market: &mut SpotMarket, cumulative_deposit_delta: Option, ) -> DriftResult<()> { - let perp_position = user.get_isolated_perp_position_mut(&self.market_index)?; + let perp_position = user.force_get_isolated_perp_position_mut(self.market_index)?; update_spot_balances( token_amount, From 8062d60241f99350ce6cc351b9e3e7019a1f8609 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 31 Jul 2025 18:30:39 -0400 Subject: [PATCH 12/31] more updates --- programs/drift/src/controller/orders.rs | 35 ++++++++++++--- programs/drift/src/controller/pnl.rs | 8 +--- programs/drift/src/instructions/keeper.rs | 54 +++++++++++++++++------ programs/drift/src/instructions/user.rs | 29 ++++++++++-- programs/drift/src/math/liquidation.rs | 32 +++++++++++--- programs/drift/src/math/orders.rs | 9 +++- 6 files changed, 127 insertions(+), 40 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index a1cab6290c..695e2179e1 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -115,6 +115,7 @@ pub fn place_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + Some(params.market_index), )?; } @@ -1047,6 +1048,7 @@ pub fn fill_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + Some(market_index), ) { Ok(_) => {} Err(_) => { @@ -1737,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], @@ -1772,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 = BTreeMap::new(); + let mut maker_fills: BTreeMap = 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 { @@ -1849,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(), @@ -1882,6 +1888,7 @@ fn fulfill_perp_order( maker_key, maker_direction, maker_fill_base_asset_amount, + maker_is_isolated_position, )?; } @@ -1904,7 +1911,7 @@ fn fulfill_perp_order( quote_asset_amount )?; - let total_maker_fill = maker_fills.values().sum::(); + let total_maker_fill = maker_fills.values().map(|(fill, _)| fill).sum::(); validate!( total_maker_fill.unsigned_abs() <= base_asset_amount, @@ -1934,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, @@ -1961,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 { @@ -1992,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, @@ -2060,20 +2075,21 @@ fn get_referrer<'a>( #[inline(always)] fn update_maker_fills_map( - map: &mut BTreeMap, + map: &mut BTreeMap, maker_key: &Pubkey, maker_direction: PositionDirection, fill: u64, + is_isolated_position: bool, ) -> DriftResult { let signed_fill = match maker_direction { PositionDirection::Long => fill.cast::()?, PositionDirection::Short => -fill.cast::()?, }; - 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(()) @@ -2958,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)?; @@ -3381,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)?; @@ -3725,6 +3743,7 @@ pub fn fill_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + None, ) { Ok(_) => {} Err(_) => { @@ -4241,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 = BTreeMap::new(); + let mut maker_fills: BTreeMap = 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 { @@ -4283,6 +4302,7 @@ fn fulfill_spot_order( maker_key, maker_direction, base_filled, + false, )?; } @@ -5205,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)?; diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index cff0b45ccb..5c0648c0b5 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -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; @@ -83,11 +81,7 @@ 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 = if user.perp_positions[position_index].is_isolated() { - Some(market_index) - } else { - None - }; + 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 { diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index a272f8e06e..10d06c7fd0 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,4 +1,6 @@ use std::cell::RefMut; +use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::convert::TryFrom; use anchor_lang::prelude::*; @@ -2721,35 +2723,59 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let custom_margin_ratio_before = user.max_margin_ratio; user.max_margin_ratio = 0; + let margin_buffer= MARGIN_PRECISION / 100; // 1% buffer let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, &perp_market_map, &spot_market_map, &mut oracle_map, MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(MARGIN_PRECISION / 100), // 1% buffer + .margin_buffer(margin_buffer), )?; - user.max_margin_ratio = custom_margin_ratio_before; + let meets_cross_margin_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); + + let isolated_position_market_indexes = user.perp_positions.iter().filter(|p| p.is_isolated()).map(|p| p.market_index).collect::>(); + + let mut isolated_position_margin_calcs : BTreeMap = BTreeMap::new(); + + for market_index in isolated_position_market_indexes { + let isolated_position_margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial) + .margin_buffer(margin_buffer) + .isolated_position_market_index(market_index), + )?; + + isolated_position_margin_calcs.insert(market_index, isolated_position_margin_calc.meets_margin_requirement_with_buffer()); + } - if margin_calc.num_perp_liabilities > 0 { - let mut requires_invariant_check = false; + user.max_margin_ratio = custom_margin_ratio_before; + if margin_calc.num_perp_liabilities > 0 || isolated_position_margin_calcs.len() > 0 { for position in user.perp_positions.iter().filter(|p| !p.is_available()) { let perp_market = perp_market_map.get_ref(&position.market_index)?; if perp_market.is_high_leverage_mode_enabled() { - requires_invariant_check = true; - break; // Exit early if invariant check is required + if position.is_isolated() { + let meets_isolated_position_margin_calc = isolated_position_margin_calcs.get(&position.market_index).unwrap(); + validate!( + *meets_isolated_position_margin_calc, + ErrorCode::DefaultError, + "User does not meet margin requirement with buffer for isolated position (market index = {})", + position.market_index + )?; + } else { + validate!( + meets_cross_margin_margin_calc, + ErrorCode::DefaultError, + "User does not meet margin requirement with buffer" + )?; + } } } - - if requires_invariant_check { - validate!( - margin_calc.meets_margin_requirement_with_buffer(), - ErrorCode::DefaultError, - "User does not meet margin requirement with buffer" - )?; - } } // only check if signer is not user authority diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index ec68ed4306..a12a12a290 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1731,13 +1731,17 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let ( from_existing_quote_entry_amount, from_existing_base_asset_amount, + from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, + to_user_is_isolated_position, ) = { let mut market = perp_market_map.get_ref_mut(&market_index)?; let from_user_position = from_user.force_get_perp_position_mut(market_index)?; + let from_user_is_isolated_position = from_user_position.is_isolated(); + let (from_existing_quote_entry_amount, from_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1749,6 +1753,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let to_user_position = to_user.force_get_perp_position_mut(market_index)?; + let to_user_is_isolated_position = to_user_position.is_isolated(); + let (to_existing_quote_entry_amount, to_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1764,19 +1770,27 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ( from_existing_quote_entry_amount, from_existing_base_asset_amount, + from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, + to_user_is_isolated_position, ) }; + let mut from_user_margin_context = MarginContext::standard(MarginRequirementType::Maintenance) + .fuel_perp_delta(market_index, transfer_amount); + + if from_user_is_isolated_position { + from_user_margin_context = from_user_margin_context.isolated_position_market_index(market_index); + } + let from_user_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &from_user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance) - .fuel_perp_delta(market_index, transfer_amount), + from_user_margin_context, )?; validate!( @@ -1785,14 +1799,20 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( "from user margin requirement is greater than total collateral" )?; + let mut to_user_margin_context = MarginContext::standard(MarginRequirementType::Initial) + .fuel_perp_delta(market_index, -transfer_amount); + + if to_user_is_isolated_position { + to_user_margin_context = to_user_margin_context.isolated_position_market_index(market_index); + } + let to_user_margin_requirement = calculate_margin_requirement_and_total_collateral_and_liability_info( &to_user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .fuel_perp_delta(market_index, -transfer_amount), + to_user_margin_context, )?; validate!( @@ -3813,6 +3833,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, ctx.accounts.state.liquidation_margin_buffer_ratio, + None, )?; let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 4035719290..e035c019bf 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -224,18 +224,36 @@ pub fn validate_user_not_being_liquidated( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, + perp_market_index: Option, ) -> DriftResult { if !user.is_being_liquidated() { return Ok(()); } - let is_still_being_liquidated = is_user_being_liquidated( - user, - market_map, - spot_market_map, - oracle_map, - liquidation_margin_buffer_ratio, - )?; + let is_isolated_perp_market = if let Some(perp_market_index) = perp_market_index { + user.force_get_perp_position_mut(perp_market_index)?.is_isolated() + } else { + false + }; + + let is_still_being_liquidated = if is_isolated_perp_market { + is_isolated_position_being_liquidated( + user, + market_map, + spot_market_map, + oracle_map, + perp_market_index.unwrap(), + liquidation_margin_buffer_ratio, + )? + } else { + is_user_being_liquidated( + user, + market_map, + spot_market_map, + oracle_map, + liquidation_margin_buffer_ratio, + )? + }; if is_still_being_liquidated { return Err(ErrorCode::UserIsBeingLiquidated); diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index ec146bd1f7..c608e922a5 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -845,6 +845,13 @@ pub fn calculate_max_perp_order_size( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + let mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); + + if is_isolated_position { + margin_context = margin_context.isolated_position_market_index(user.perp_positions[position_index].market_index); + } + // calculate initial margin requirement let MarginCalculation { margin_requirement, @@ -855,7 +862,7 @@ pub fn calculate_max_perp_order_size( perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Initial).strict(true), + margin_context, )?; let user_custom_margin_ratio = user.max_margin_ratio; From 991dda9e5f0ce2c80c3ff32536c202dcd21d3486 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 4 Aug 2025 17:52:04 -0400 Subject: [PATCH 13/31] always calc isolated pos --- programs/drift/src/controller/liquidation.rs | 12 +- programs/drift/src/math/margin.rs | 218 ++++-------------- .../drift/src/state/margin_calculation.rs | 96 +++++++- programs/drift/src/state/user.rs | 4 +- 4 files changed, 141 insertions(+), 189 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index e4bec91158..07e756f7a1 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -271,7 +271,7 @@ pub fn liquidate_perp( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if user.perp_positions[position_index].base_asset_amount == 0 { @@ -898,7 +898,7 @@ pub fn liquidate_perp_with_fill( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if user.perp_positions[position_index].base_asset_amount == 0 { @@ -1466,7 +1466,7 @@ pub fn liquidate_spot( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; let margin_shortage = intermediate_margin_calculation.margin_shortage()?; @@ -1997,7 +1997,7 @@ pub fn liquidate_spot_with_swap_begin( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; let margin_shortage = intermediate_margin_calculation.margin_shortage()?; @@ -2569,7 +2569,7 @@ pub fn liquidate_borrow_for_perp_pnl( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; let margin_shortage = intermediate_margin_calculation.margin_shortage()?; @@ -3053,7 +3053,7 @@ pub fn liquidate_perp_pnl_for_deposit( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if is_contract_tier_violation { diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index aa81af0add..d2f7ef16d9 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,6 +6,7 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; +use crate::state::margin_calculation::IsolatedPositionMarginCalculation; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -236,10 +237,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_map: &mut OracleMap, context: MarginContext, ) -> DriftResult { - if context.isolated_position_market_index.is_some() { - return calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position(user, perp_market_map, spot_market_map, oracle_map, context); - } - let mut calculation = MarginCalculation::new(context); let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { @@ -501,10 +498,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( continue; } - if market_position.is_isolated() { - continue; - } - let market = &perp_market_map.get_ref(&market_position.market_index)?; validate!( @@ -570,17 +563,45 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_price_data.price, )?; - calculation.add_margin_requirement( - perp_margin_requirement, - worst_case_liability_value, - MarketIdentifier::perp(market.market_index), - )?; + if market_position.is_isolated() { + let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; + let quote_token_amount = get_token_amount( + market_position + .isolated_position_scaled_balance + .cast::()?, + "e_spot_market, + &SpotBalanceType::Deposit, + )?; + + let quote_token_value = get_strict_token_value( + quote_token_amount.cast::()?, + quote_spot_market.decimals, + &strict_quote_price, + )?; - if calculation.track_open_orders_fraction() { - calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; - } + calculation.add_isolated_position_margin_calculation( + market.market_index, + quote_token_value, + weighted_pnl, + worst_case_liability_value, + perp_margin_requirement, + )?; - calculation.add_total_collateral(weighted_pnl)?; + #[cfg(feature = "drift-rs")] + calculation.add_spot_asset_value(quote_token_value)?; + } else { + calculation.add_margin_requirement( + perp_margin_requirement, + worst_case_liability_value, + MarketIdentifier::perp(market.market_index), + )?; + + if calculation.track_open_orders_fraction() { + calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; + } + + calculation.add_total_collateral(weighted_pnl)?; + } #[cfg(feature = "drift-rs")] calculation.add_perp_liability_value(worst_case_liability_value)?; @@ -645,168 +666,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( Ok(calculation) } -pub fn calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position( - user: &User, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - context: MarginContext, -) -> DriftResult { - let mut calculation = MarginCalculation::new(context); - - let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { - user.max_margin_ratio - } else { - 0_u32 - }; - - if let Some(margin_ratio_override) = context.margin_ratio_override { - user_custom_margin_ratio = margin_ratio_override.max(user_custom_margin_ratio); - } - - let user_pool_id = user.pool_id; - let user_high_leverage_mode = user.is_high_leverage_mode(); - - let isolated_position_market_index = context.isolated_position_market_index.unwrap(); - - let perp_position = user.get_perp_position(isolated_position_market_index)?; - - let perp_market = perp_market_map.get_ref(&isolated_position_market_index)?; - - validate!( - user_pool_id == perp_market.pool_id, - ErrorCode::InvalidPoolId, - "user pool id ({}) == perp market pool id ({})", - user_pool_id, - perp_market.pool_id, - )?; - - let quote_spot_market = spot_market_map.get_ref(&perp_market.quote_spot_market_index)?; - - validate!( - user_pool_id == quote_spot_market.pool_id, - ErrorCode::InvalidPoolId, - "user pool id ({}) == quote spot market pool id ({})", - user_pool_id, - quote_spot_market.pool_id, - )?; - - let (quote_oracle_price_data, quote_oracle_validity) = oracle_map.get_price_data_and_validity( - MarketType::Spot, - quote_spot_market.market_index, - "e_spot_market.oracle_id(), - quote_spot_market - .historical_oracle_data - .last_oracle_price_twap, - quote_spot_market.get_max_confidence_interval_multiplier()?, - 0, - )?; - - let quote_oracle_valid = - is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; - - let quote_strict_oracle_price = StrictOraclePrice::new( - quote_oracle_price_data.price, - quote_spot_market - .historical_oracle_data - .last_oracle_price_twap_5min, - calculation.context.strict, - ); - quote_strict_oracle_price.validate()?; - - let quote_token_amount = get_token_amount( - perp_position - .isolated_position_scaled_balance - .cast::()?, - "e_spot_market, - &SpotBalanceType::Deposit, - )?; - - let quote_token_value = get_strict_token_value( - quote_token_amount.cast::()?, - quote_spot_market.decimals, - "e_strict_oracle_price, - )?; - - calculation.add_total_collateral(quote_token_value)?; - - calculation.update_all_deposit_oracles_valid(quote_oracle_valid); - - #[cfg(feature = "drift-rs")] - calculation.add_spot_asset_value(quote_token_value)?; - - let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( - MarketType::Perp, - isolated_position_market_index, - &perp_market.oracle_id(), - perp_market - .amm - .historical_oracle_data - .last_oracle_price_twap, - perp_market.get_max_confidence_interval_multiplier()?, - 0, - )?; - - let ( - perp_margin_requirement, - weighted_pnl, - worst_case_liability_value, - open_order_margin_requirement, - base_asset_value, - ) = calculate_perp_position_value_and_pnl( - &perp_position, - &perp_market, - oracle_price_data, - "e_strict_oracle_price, - context.margin_type, - user_custom_margin_ratio, - user_high_leverage_mode, - calculation.track_open_orders_fraction(), - )?; - - calculation.add_margin_requirement( - perp_margin_requirement, - worst_case_liability_value, - MarketIdentifier::perp(isolated_position_market_index), - )?; - - calculation.add_total_collateral(weighted_pnl)?; - - #[cfg(feature = "drift-rs")] - calculation.add_perp_liability_value(worst_case_liability_value)?; - #[cfg(feature = "drift-rs")] - calculation.add_perp_pnl(weighted_pnl)?; - - let has_perp_liability = perp_position.base_asset_amount != 0 - || perp_position.quote_asset_amount < 0 - || perp_position.has_open_order(); - - if has_perp_liability { - calculation.add_perp_liability()?; - calculation.update_with_perp_isolated_liability( - perp_market.contract_tier == ContractTier::Isolated, - ); - } - - if has_perp_liability || calculation.context.margin_type != MarginRequirementType::Initial { - calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( - quote_oracle_validity, - Some(DriftAction::MarginCalc), - )?); - calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( - oracle_validity, - Some(DriftAction::MarginCalc), - )?); - } - - calculation.validate_num_spot_liabilities()?; - - Ok(calculation) -} - pub fn validate_any_isolated_tier_requirements( user: &User, - calculation: MarginCalculation, + calculation: &MarginCalculation, ) -> DriftResult { if calculation.with_perp_isolated_liability && !user.is_reduce_only() { validate!( @@ -880,7 +742,7 @@ pub fn meets_place_order_margin_requirement( return Err(ErrorCode::InsufficientCollateral); } - validate_any_isolated_tier_requirements(user, calculation)?; + validate_any_isolated_tier_requirements(user, &calculation)?; Ok(()) } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 1756a5a7b8..038e87cdf5 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; @@ -183,7 +185,7 @@ impl MarginContext { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct MarginCalculation { pub context: MarginContext, pub total_collateral: i128, @@ -196,6 +198,7 @@ pub struct MarginCalculation { margin_requirement_plus_buffer: u128, #[cfg(test)] pub margin_requirement_plus_buffer: u128, + pub isolated_position_margin_calculation: BTreeMap, pub num_spot_liabilities: u8, pub num_perp_liabilities: u8, pub all_deposit_oracles_valid: bool, @@ -213,6 +216,29 @@ pub struct MarginCalculation { pub fuel_positions: u32, } +#[derive(Clone, Copy, Debug, Default)] +pub struct IsolatedPositionMarginCalculation { + pub margin_requirement: u128, + pub total_collateral: i128, + pub total_collateral_buffer: i128, + pub margin_requirement_plus_buffer: u128, +} + +impl IsolatedPositionMarginCalculation { + + pub fn get_total_collateral_plus_buffer(&self) -> i128 { + self.total_collateral.saturating_add(self.total_collateral_buffer) + } + + pub fn meets_margin_requirement(&self) -> bool { + self.total_collateral >= self.margin_requirement as i128 + } + + pub fn meets_margin_requirement_with_buffer(&self) -> bool { + self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + } +} + impl MarginCalculation { pub fn new(context: MarginContext) -> Self { Self { @@ -221,6 +247,7 @@ impl MarginCalculation { total_collateral_buffer: 0, margin_requirement: 0, margin_requirement_plus_buffer: 0, + isolated_position_margin_calculation: BTreeMap::new(), num_spot_liabilities: 0, num_perp_liabilities: 0, all_deposit_oracles_valid: true, @@ -280,6 +307,41 @@ impl MarginCalculation { Ok(()) } + pub fn add_isolated_position_margin_calculation(&mut self, market_index: u16, deposit_value: i128, pnl: i128, liability_value: u128, margin_requirement: u128) -> DriftResult { + let total_collateral = deposit_value.cast::()?.safe_add(pnl)?; + + let total_collateral_buffer = if self.context.margin_buffer > 0 && pnl < 0 { + pnl.safe_mul(self.context.margin_buffer.cast::()?)? / MARGIN_PRECISION_I128 + } else { + 0 + }; + + let margin_requirement_plus_buffer = if self.context.margin_buffer > 0 { + margin_requirement.safe_add(liability_value.safe_mul(self.context.margin_buffer)? / MARGIN_PRECISION_U128)? + } else { + 0 + }; + + let isolated_position_margin_calculation = IsolatedPositionMarginCalculation { + margin_requirement, + total_collateral, + total_collateral_buffer, + margin_requirement_plus_buffer, + }; + + self.isolated_position_margin_calculation.insert(market_index, isolated_position_margin_calculation); + + if let Some(market_to_track) = self.market_to_track_margin_requirement() { + if market_to_track == MarketIdentifier::perp(market_index) { + self.tracked_market_margin_requirement = self + .tracked_market_margin_requirement + .safe_add(margin_requirement_plus_buffer)?; + } + } + + Ok(()) + } + pub fn add_open_orders_margin_requirement(&mut self, margin_requirement: u128) -> DriftResult { self.open_orders_margin_requirement = self .open_orders_margin_requirement @@ -365,11 +427,39 @@ impl MarginCalculation { } pub fn meets_margin_requirement(&self) -> bool { - self.total_collateral >= self.margin_requirement as i128 + let cross_margin_meets_margin_requirement = self.total_collateral >= self.margin_requirement as i128; + + if !cross_margin_meets_margin_requirement { + msg!("cross margin margin calculation doesnt meet margin requirement"); + return false; + } + + for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + if !isolated_position_margin_calculation.meets_margin_requirement() { + msg!("isolated position margin calculation for market {} does not meet margin requirement", market_index); + return false; + } + } + + true } pub fn meets_margin_requirement_with_buffer(&self) -> bool { - self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + let cross_margin_meets_margin_requirement = self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128; + + if !cross_margin_meets_margin_requirement { + msg!("cross margin margin calculation doesnt meet margin requirement with buffer"); + return false; + } + + for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { + msg!("isolated position margin calculation for market {} does not meet margin requirement with buffer", market_index); + return false; + } + } + + true } pub fn positions_meets_margin_requirement(&self) -> DriftResult { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index f32c7dd9f7..8c4f70a82a 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -609,7 +609,7 @@ impl User { )?; } - validate_any_isolated_tier_requirements(self, calculation)?; + validate_any_isolated_tier_requirements(self, &calculation)?; validate!( calculation.meets_margin_requirement(), @@ -670,7 +670,7 @@ impl User { )?; } - validate_any_isolated_tier_requirements(self, calculation)?; + validate_any_isolated_tier_requirements(self, &calculation)?; validate!( calculation.meets_margin_requirement(), From c627c1e8c58ebb31a3ca81ffd4c559f6a68d57c3 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 4 Aug 2025 18:29:09 -0400 Subject: [PATCH 14/31] rm isolated position market index logic --- programs/drift/src/controller/liquidation.rs | 10 ++---- programs/drift/src/controller/orders.rs | 14 ++------ programs/drift/src/controller/pnl.rs | 5 +-- programs/drift/src/controller/pnl/tests.rs | 1 - programs/drift/src/instructions/keeper.rs | 27 ++++----------- programs/drift/src/instructions/user.rs | 2 +- programs/drift/src/math/margin.rs | 34 +++---------------- .../drift/src/state/margin_calculation.rs | 11 +++--- 8 files changed, 26 insertions(+), 78 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 07e756f7a1..86d2d4877d 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -559,10 +559,8 @@ pub fn liquidate_perp( liquidation_mode.enter_bankruptcy(user); } - let liquidator_isolated_position_market_index = liquidator.get_perp_position(market_index)?.is_isolated().then_some(market_index); - let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; validate!( liquidator_meets_initial_margin_requirement, @@ -2722,7 +2720,7 @@ pub fn liquidate_borrow_for_perp_pnl( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; validate!( liquidator_meets_initial_margin_requirement, @@ -3207,10 +3205,8 @@ pub fn liquidate_perp_pnl_for_deposit( liquidation_mode.enter_bankruptcy(user)?; } - let liquidator_isolated_position_market_index = liquidator.get_perp_position(perp_market_index)?.is_isolated().then_some(perp_market_index); - let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; validate!( liquidator_meets_initial_margin_requirement, diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 695e2179e1..aeeb19c0ed 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -357,21 +357,14 @@ 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 || isolated_position_market_index.is_some()) && !options.is_liquidation() { + if options.enforce_margin_check && !options.is_liquidation() { meets_place_order_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, options.risk_increasing, - isolated_position_market_index, )?; } @@ -3107,7 +3100,7 @@ pub fn trigger_order( }; let meets_initial_margin_requirement = - meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?; + meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; if !meets_initial_margin_requirement { cancel_order( @@ -3611,7 +3604,6 @@ pub fn place_spot_order( spot_market_map, oracle_map, options.risk_increasing, - None, )?; } @@ -5375,7 +5367,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, None)?; + meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; if !meets_initial_margin_requirement { cancel_order( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 5c0648c0b5..a5a59a5f1b 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -81,18 +81,15 @@ 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) if !isolated_position_market_index.is_some() => meets_margin_requirement, + Some(meets_margin_requirement) => meets_margin_requirement, // TODO check margin for isolate position _ => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, - isolated_position_market_index, )?, }; diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index ee6dd872b7..8d755bcfff 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -410,7 +410,6 @@ pub fn user_does_not_meet_strict_maintenance_requirement() { &market_map, &spot_market_map, &mut oracle_map, - None, ) .unwrap(); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 10d06c7fd0..34cbab1dfa 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -991,25 +991,12 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - let mut try_cache_margin_requirement = false; - for market_index in market_indexes.iter() { - if !user.get_perp_position(*market_index)?.is_isolated() { - try_cache_margin_requirement = true; - break; - } - } - - let meets_margin_requirement = if try_cache_margin_requirement { - Some(meets_settle_pnl_maintenance_margin_requirement( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - None, - )?) - } else { - None - }; + let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; for market_index in market_indexes.iter() { let market_in_settlement = @@ -1050,7 +1037,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( &mut oracle_map, &clock, state, - meets_margin_requirement, + Some(meets_margin_requirement), mode, ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a12a12a290..236823e433 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3536,7 +3536,7 @@ pub fn handle_update_user_pool_id<'c: 'info, 'info>( user.pool_id = pool_id; // will throw if user has deposits/positions in other pools - meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map, None)?; + meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; Ok(()) } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index d2f7ef16d9..94b42d8589 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,7 +6,6 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; -use crate::state::margin_calculation::IsolatedPositionMarginCalculation; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -711,34 +710,23 @@ pub fn meets_place_order_margin_requirement( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, risk_increasing: bool, - isolated_position_market_index: Option, ) -> DriftResult { let margin_type = if risk_increasing { MarginRequirementType::Initial } else { MarginRequirementType::Maintenance }; - let context = MarginContext::standard(margin_type).strict(true); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(margin_type).strict(true), )?; if !calculation.meets_margin_requirement() { - msg!( - "total_collateral={}, margin_requirement={} margin type = {:?}", - calculation.total_collateral, - calculation.margin_requirement, - margin_type - ); + calculation.print_margin_calculations(); return Err(ErrorCode::InsufficientCollateral); } @@ -752,20 +740,13 @@ pub fn meets_initial_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - isolated_position_market_index: Option, ) -> DriftResult { - let context = MarginContext::standard(MarginRequirementType::Initial); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } - calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(MarginRequirementType::Initial), ) .map(|calc| calc.meets_margin_requirement()) } @@ -775,20 +756,13 @@ pub fn meets_settle_pnl_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - isolated_position_market_index: Option, ) -> DriftResult { - let context = MarginContext::standard(MarginRequirementType::Maintenance).strict(true); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } - calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(MarginRequirementType::Maintenance).strict(true), ) .map(|calc| calc.meets_margin_requirement()) } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 038e87cdf5..f9b6e9f2fa 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -430,13 +430,11 @@ impl MarginCalculation { let cross_margin_meets_margin_requirement = self.total_collateral >= self.margin_requirement as i128; if !cross_margin_meets_margin_requirement { - msg!("cross margin margin calculation doesnt meet margin requirement"); return false; } for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement() { - msg!("isolated position margin calculation for market {} does not meet margin requirement", market_index); return false; } } @@ -448,13 +446,11 @@ impl MarginCalculation { let cross_margin_meets_margin_requirement = self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128; if !cross_margin_meets_margin_requirement { - msg!("cross margin margin calculation doesnt meet margin requirement with buffer"); return false; } for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { - msg!("isolated position margin calculation for market {} does not meet margin requirement with buffer", market_index); return false; } } @@ -462,6 +458,13 @@ impl MarginCalculation { true } + pub fn print_margin_calculations(&self) { + msg!("cross_margin margin_requirement={}, total_collateral={}", self.margin_requirement, self.total_collateral); + for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + msg!("isolated_position for market {}: margin_requirement={}, total_collateral={}", market_index, isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral); + } + } + pub fn positions_meets_margin_requirement(&self) -> DriftResult { Ok(self.total_collateral >= self From a00f3a98ab4001ad10b9c9d4c30ce8d21831edee Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 11:24:15 -0400 Subject: [PATCH 15/31] moar --- programs/drift/src/controller/orders.rs | 29 +++++-------------- programs/drift/src/controller/pnl.rs | 8 +---- programs/drift/src/controller/pnl/tests.rs | 2 +- programs/drift/src/instructions/user.rs | 29 ++++--------------- programs/drift/src/math/margin.rs | 9 +----- .../drift/src/state/margin_calculation.rs | 1 + 6 files changed, 17 insertions(+), 61 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index aeeb19c0ed..467b27ec16 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1732,8 +1732,6 @@ 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], @@ -1769,7 +1767,7 @@ fn fulfill_perp_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut maker_fills: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = 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 { @@ -1846,8 +1844,6 @@ 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(), @@ -1881,7 +1877,6 @@ fn fulfill_perp_order( maker_key, maker_direction, maker_fill_base_asset_amount, - maker_is_isolated_position, )?; } @@ -1904,7 +1899,7 @@ fn fulfill_perp_order( quote_asset_amount )?; - let total_maker_fill = maker_fills.values().map(|(fill, _)| fill).sum::(); + let total_maker_fill = maker_fills.values().sum::(); validate!( total_maker_fill.unsigned_abs() <= base_asset_amount, @@ -1934,10 +1929,6 @@ 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, @@ -1965,7 +1956,7 @@ fn fulfill_perp_order( } } - for (maker_key, (maker_base_asset_amount_filled, maker_is_isolated_position)) 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 { @@ -1996,10 +1987,6 @@ 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, @@ -2068,21 +2055,20 @@ fn get_referrer<'a>( #[inline(always)] fn update_maker_fills_map( - map: &mut BTreeMap, + map: &mut BTreeMap, maker_key: &Pubkey, maker_direction: PositionDirection, fill: u64, - is_isolated_position: bool, ) -> DriftResult { let signed_fill = match maker_direction { PositionDirection::Long => fill.cast::()?, PositionDirection::Short => -fill.cast::()?, }; - 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, is_isolated_position)); + map.insert(*maker_key, signed_fill); } Ok(()) @@ -4252,7 +4238,7 @@ fn fulfill_spot_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut maker_fills: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = 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 { @@ -4294,7 +4280,6 @@ fn fulfill_spot_order( maker_key, maker_direction, base_filled, - false, )?; } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index a5a59a5f1b..65d6364be3 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -349,14 +349,8 @@ 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, isolated_position_market_index)?) + if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) { return Err(ErrorCode::InsufficientCollateralForSettlingPNL); } diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 8d755bcfff..4a35df4e49 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -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, None) + meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map) .unwrap(); assert_eq!(meets_maintenance, true); diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 236823e433..6755b1fee0 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3762,29 +3762,12 @@ pub fn handle_enable_user_high_leverage_mode<'c: 'info, 'info>( "user already in high leverage mode" )?; - let has_non_isolated_position = user.perp_positions.iter().any(|position| !position.is_isolated()); - - if has_non_isolated_position { - meets_maintenance_margin_requirement( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - None, - )?; - } - - let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::>(); - - for market_index in isolated_position_market_indexes.iter() { - meets_maintenance_margin_requirement( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - Some(*market_index), - )?; - } + meets_maintenance_margin_requirement( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; let mut config = load_mut!(ctx.accounts.high_leverage_mode_config)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 94b42d8589..65dfe7a7a2 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -772,20 +772,13 @@ pub fn meets_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - isolated_position_market_index: Option, ) -> DriftResult { - let context = MarginContext::standard(MarginRequirementType::Maintenance); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } - calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(MarginRequirementType::Maintenance), ) .map(|calc| calc.meets_margin_requirement()) } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index f9b6e9f2fa..aac4dccd7f 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -510,6 +510,7 @@ impl MarginCalculation { .safe_div(self.margin_requirement) } + // todo check every where this is used pub fn get_free_collateral(&self) -> DriftResult { self.total_collateral .safe_sub(self.margin_requirement.cast::()?)? From d435dad769e1aa6a9c7642e0123ffc5673ae66a8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 12:46:49 -0400 Subject: [PATCH 16/31] program: rm the isolated position market index --- programs/drift/src/controller/liquidation.rs | 25 +++++------ programs/drift/src/instructions/keeper.rs | 42 ++++--------------- programs/drift/src/instructions/user.rs | 19 --------- programs/drift/src/math/liquidation.rs | 3 +- programs/drift/src/math/orders.rs | 6 +-- programs/drift/src/state/liquidation_mode.rs | 2 +- .../drift/src/state/margin_calculation.rs | 8 ---- programs/drift/src/state/user.rs | 9 +--- 8 files changed, 25 insertions(+), 89 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 86d2d4877d..5a383254e7 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3681,26 +3681,27 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; + // todo handle this if !user.is_being_liquidated() && !margin_calculation.meets_margin_requirement() { user.enter_liquidation(slot)?; } let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); - for market_index in isolated_position_market_indexes { - let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - user, - perp_market_map, - spot_market_map, - oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), - )?; + // for market_index in isolated_position_market_indexes { + // let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + // user, + // perp_market_map, + // spot_market_map, + // oracle_map, + // MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), + // )?; - if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { - user.enter_isolated_position_liquidation(market_index)?; - } + // if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { + // user.enter_isolated_position_liquidation(market_index)?; + // } - } + // } Ok(()) } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 34cbab1dfa..6efb3d7fdc 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2720,47 +2720,19 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( .margin_buffer(margin_buffer), )?; - let meets_cross_margin_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); - - let isolated_position_market_indexes = user.perp_positions.iter().filter(|p| p.is_isolated()).map(|p| p.market_index).collect::>(); - - let mut isolated_position_margin_calcs : BTreeMap = BTreeMap::new(); - - for market_index in isolated_position_market_indexes { - let isolated_position_margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(margin_buffer) - .isolated_position_market_index(market_index), - )?; - - isolated_position_margin_calcs.insert(market_index, isolated_position_margin_calc.meets_margin_requirement_with_buffer()); - } + let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); user.max_margin_ratio = custom_margin_ratio_before; - if margin_calc.num_perp_liabilities > 0 || isolated_position_margin_calcs.len() > 0 { + if margin_calc.num_perp_liabilities > 0 { for position in user.perp_positions.iter().filter(|p| !p.is_available()) { let perp_market = perp_market_map.get_ref(&position.market_index)?; if perp_market.is_high_leverage_mode_enabled() { - if position.is_isolated() { - let meets_isolated_position_margin_calc = isolated_position_margin_calcs.get(&position.market_index).unwrap(); - validate!( - *meets_isolated_position_margin_calc, - ErrorCode::DefaultError, - "User does not meet margin requirement with buffer for isolated position (market index = {})", - position.market_index - )?; - } else { - validate!( - meets_cross_margin_margin_calc, - ErrorCode::DefaultError, - "User does not meet margin requirement with buffer" - )?; - } + validate!( + meets_margin_calc, + ErrorCode::DefaultError, + "User does not meet margin requirement with buffer" + )?; } } } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 6755b1fee0..c4dbe8eaa2 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -787,7 +787,6 @@ pub fn handle_withdraw<'c: 'info, 'info>( amount as u128, &mut user_stats, now, - None, )?; validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; @@ -962,7 +961,6 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( amount as u128, user_stats, now, - None, )?; validate_spot_margin_trading( @@ -1731,17 +1729,13 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let ( from_existing_quote_entry_amount, from_existing_base_asset_amount, - from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, - to_user_is_isolated_position, ) = { let mut market = perp_market_map.get_ref_mut(&market_index)?; let from_user_position = from_user.force_get_perp_position_mut(market_index)?; - let from_user_is_isolated_position = from_user_position.is_isolated(); - let (from_existing_quote_entry_amount, from_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1753,8 +1747,6 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let to_user_position = to_user.force_get_perp_position_mut(market_index)?; - let to_user_is_isolated_position = to_user_position.is_isolated(); - let (to_existing_quote_entry_amount, to_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1770,20 +1762,14 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ( from_existing_quote_entry_amount, from_existing_base_asset_amount, - from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, - to_user_is_isolated_position, ) }; let mut from_user_margin_context = MarginContext::standard(MarginRequirementType::Maintenance) .fuel_perp_delta(market_index, transfer_amount); - if from_user_is_isolated_position { - from_user_margin_context = from_user_margin_context.isolated_position_market_index(market_index); - } - let from_user_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &from_user, @@ -1802,10 +1788,6 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let mut to_user_margin_context = MarginContext::standard(MarginRequirementType::Initial) .fuel_perp_delta(market_index, -transfer_amount); - if to_user_is_isolated_position { - to_user_margin_context = to_user_margin_context.isolated_position_market_index(market_index); - } - let to_user_margin_requirement = calculate_margin_requirement_and_total_collateral_and_liability_info( &to_user, @@ -2185,7 +2167,6 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( amount as u128, user_stats, now, - None, )?; validate_spot_margin_trading( diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index e035c019bf..cf425bade7 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -264,6 +264,7 @@ pub fn validate_user_not_being_liquidated( Ok(()) } +// todo check if this is corrects pub fn is_isolated_position_being_liquidated( user: &User, market_map: &PerpMarketMap, @@ -277,7 +278,7 @@ pub fn is_isolated_position_being_liquidated( market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(perp_market_index), + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index c608e922a5..879bc4e9fe 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -847,11 +847,6 @@ pub fn calculate_max_perp_order_size( ) -> DriftResult { let is_isolated_position = user.perp_positions[position_index].is_isolated(); let mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); - - if is_isolated_position { - margin_context = margin_context.isolated_position_market_index(user.perp_positions[position_index].market_index); - } - // calculate initial margin requirement let MarginCalculation { margin_requirement, @@ -868,6 +863,7 @@ pub fn calculate_max_perp_order_size( let user_custom_margin_ratio = user.max_margin_ratio; let user_high_leverage_mode = user.is_high_leverage_mode(); + // todo check if this is correct let free_collateral_before = total_collateral.safe_sub(margin_requirement.cast()?)?; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index a86d5a929f..edf8b99c7c 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -187,7 +187,7 @@ impl IsolatedLiquidatePerpMode { impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(self.market_index)) + Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) } fn user_is_being_liquidated(&self, user: &User) -> DriftResult { diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index aac4dccd7f..874b6d4e45 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -37,7 +37,6 @@ pub struct MarginContext { pub fuel_perp_delta: Option<(u16, i64)>, pub fuel_spot_deltas: [(u16, i128); 2], pub margin_ratio_override: Option, - pub isolated_position_market_index: Option, } #[derive(PartialEq, Eq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] @@ -77,7 +76,6 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, - isolated_position_market_index: None, } } @@ -156,7 +154,6 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, - isolated_position_market_index: None, } } @@ -178,11 +175,6 @@ impl MarginContext { } Ok(self) } - - pub fn isolated_position_market_index(mut self, market_index: u16) -> Self { - self.isolated_position_market_index = Some(market_index); - self - } } #[derive(Clone, Debug)] diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 8c4f70a82a..36dd5f7aa3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -640,7 +640,6 @@ impl User { withdraw_amount: u128, user_stats: &mut UserStats, now: i64, - isolated_perp_position_market_index: Option, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; let context = MarginContext::standard(margin_requirement_type) @@ -649,11 +648,6 @@ impl User { .fuel_spot_delta(withdraw_market_index, withdraw_amount.cast::()?) .fuel_numerator(self, now); - // TODO check if this is correct - if let Some(isolated_perp_position_market_index) = isolated_perp_position_market_index { - context.isolated_position_market_index(isolated_perp_position_market_index); - } - let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, perp_market_map, @@ -703,8 +697,7 @@ impl User { ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; let context = MarginContext::standard(margin_requirement_type) - .strict(strict) - .isolated_position_market_index(isolated_perp_position_market_index); + .strict(strict); let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, From ed76b47bc5219d1de4033f41e9d982aa57dd8657 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 12:54:04 -0400 Subject: [PATCH 17/31] some tweaks --- programs/drift/src/controller/orders.rs | 6 ------ programs/drift/src/instructions/keeper.rs | 2 -- 2 files changed, 8 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 467b27ec16..dc468ac868 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -3079,12 +3079,6 @@ 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)?; diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 6efb3d7fdc..2591439bfd 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,6 +1,4 @@ use std::cell::RefMut; -use std::collections::BTreeMap; -use std::collections::BTreeSet; use std::convert::TryFrom; use anchor_lang::prelude::*; From c13a605b6a3676a4487a7d9d2e881c40c92c966e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 18:24:05 -0400 Subject: [PATCH 18/31] rm some old margin code --- .../drift/src/controller/pnl/delisting.rs | 14 +++----- programs/drift/src/math/margin.rs | 21 +----------- programs/drift/src/math/margin/tests.rs | 18 ++++------- .../drift/src/state/margin_calculation.rs | 32 ++----------------- 4 files changed, 14 insertions(+), 71 deletions(-) diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index eafbd2148b..67fd12152f 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2336,7 +2336,7 @@ pub mod delisting_test { let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2345,7 +2345,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2416,7 +2415,7 @@ pub mod delisting_test { let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2425,7 +2424,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2504,7 +2502,7 @@ pub mod delisting_test { assert_eq!(market.amm.cumulative_funding_rate_short, 0); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2513,7 +2511,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2596,7 +2593,7 @@ pub mod delisting_test { assert_eq!(market.amm.cumulative_funding_rate_short, 0); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2604,8 +2601,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, - false, - false, + false ) .unwrap(); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 65dfe7a7a2..4cbbcc5818 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -103,8 +103,7 @@ pub fn calculate_perp_position_value_and_pnl( margin_requirement_type: MarginRequirementType, user_custom_margin_ratio: u32, user_high_leverage_mode: bool, - track_open_order_fraction: bool, -) -> DriftResult<(u128, i128, u128, u128, u128)> { +) -> DriftResult<(u128, i128, u128, u128)> { let valuation_price = if market.status == MarketStatus::Settlement { market.expiry_price } else { @@ -181,22 +180,10 @@ pub fn calculate_perp_position_value_and_pnl( weighted_unrealized_pnl = weighted_unrealized_pnl.min(MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN); } - let open_order_margin_requirement = - if track_open_order_fraction && worst_case_base_asset_amount != 0 { - let worst_case_base_asset_amount = worst_case_base_asset_amount.unsigned_abs(); - worst_case_base_asset_amount - .safe_sub(market_position.base_asset_amount.unsigned_abs().cast()?)? - .safe_mul(margin_requirement)? - .safe_div(worst_case_base_asset_amount)? - } else { - 0_u128 - }; - Ok(( margin_requirement, weighted_unrealized_pnl, worse_case_liability_value, - open_order_margin_requirement, base_asset_value, )) } @@ -542,7 +529,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( perp_margin_requirement, weighted_pnl, worst_case_liability_value, - open_order_margin_requirement, base_asset_value, ) = calculate_perp_position_value_and_pnl( market_position, @@ -552,7 +538,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( context.margin_type, user_custom_margin_ratio, user_high_leverage_mode, - calculation.track_open_orders_fraction(), )?; calculation.update_fuel_perp_bonus( @@ -595,10 +580,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( MarketIdentifier::perp(market.market_index), )?; - if calculation.track_open_orders_fraction() { - calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; - } - calculation.add_total_collateral(weighted_pnl)?; } diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 7a256b65db..2ad60a5b9e 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -285,7 +285,7 @@ mod test { assert_eq!(uaw, 9559); let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (pmr, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -293,7 +293,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -363,7 +362,7 @@ mod test { assert_eq!(position_unrealized_pnl * 800000, 19426229516800000); // 1.9 billion let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr_2, upnl_2, _, _, _) = calculate_perp_position_value_and_pnl( + let (pmr_2, upnl_2, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -371,7 +370,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -4084,7 +4082,7 @@ mod calculate_perp_position_value_and_pnl_prediction_market { let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4092,14 +4090,13 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); assert_eq!(margin_requirement, QUOTE_PRECISION * 3 / 4); //$.75 assert_eq!(upnl, 0); //0 - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4107,7 +4104,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); @@ -4147,7 +4143,7 @@ mod calculate_perp_position_value_and_pnl_prediction_market { let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4155,14 +4151,13 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); assert_eq!(margin_requirement, QUOTE_PRECISION * 3 / 4); //$.75 assert_eq!(upnl, 0); //0 - let (margin_requirement, upnl, _, _, _) = calculate_perp_position_value_and_pnl( + let (margin_requirement, upnl, _, _) = calculate_perp_position_value_and_pnl( &market_position, &market, &oracle_price_data, @@ -4170,7 +4165,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 874b6d4e45..bd1d9d0864 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -17,9 +17,7 @@ use anchor_lang::{prelude::*, solana_program::msg}; #[derive(Clone, Copy, Debug)] pub enum MarginCalculationMode { - Standard { - track_open_orders_fraction: bool, - }, + Standard, Liquidation { market_to_track_margin_requirement: Option, }, @@ -65,9 +63,7 @@ impl MarginContext { pub fn standard(margin_type: MarginRequirementType) -> Self { Self { margin_type, - mode: MarginCalculationMode::Standard { - track_open_orders_fraction: false, - }, + mode: MarginCalculationMode::Standard, strict: false, ignore_invalid_deposit_oracles: false, margin_buffer: 0, @@ -116,21 +112,6 @@ impl MarginContext { self } - pub fn track_open_orders_fraction(mut self) -> DriftResult { - match self.mode { - MarginCalculationMode::Standard { - track_open_orders_fraction: ref mut track, - } => { - *track = true; - } - _ => { - msg!("Cant track open orders fraction outside of standard mode"); - return Err(ErrorCode::InvalidMarginCalculation); - } - } - Ok(self) - } - pub fn margin_ratio_override(mut self, margin_ratio_override: u32) -> Self { msg!( "Applying max margin ratio override: {} due to stale oracle", @@ -526,15 +507,6 @@ impl MarginCalculation { matches!(self.context.mode, MarginCalculationMode::Liquidation { .. }) } - pub fn track_open_orders_fraction(&self) -> bool { - matches!( - self.context.mode, - MarginCalculationMode::Standard { - track_open_orders_fraction: true - } - ) - } - pub fn update_fuel_perp_bonus( &mut self, perp_market: &PerpMarket, From 4a9aadc3b9bbe9b2c4bfa227b7b4e1320b7f88cd Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 18:36:43 -0400 Subject: [PATCH 19/31] tweak meets withdraw requirements --- programs/drift/src/instructions/user.rs | 4 ---- .../drift/src/state/margin_calculation.rs | 8 +++++++ programs/drift/src/state/user.rs | 22 +++++++++---------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index c4dbe8eaa2..1d73dcf530 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2232,8 +2232,6 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - user_stats, - now, perp_market_index, )?; @@ -2355,8 +2353,6 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - &mut user_stats, - now, perp_market_index, )?; diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index bd1d9d0864..955b4da458 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -593,4 +593,12 @@ impl MarginCalculation { Ok(()) } + + pub fn get_isolated_position_margin_calculation(&self, market_index: u16) -> DriftResult<&IsolatedPositionMarginCalculation> { + if let Some(isolated_position_margin_calculation) = self.isolated_position_margin_calculation.get(&market_index) { + Ok(isolated_position_margin_calculation) + } else { + Err(ErrorCode::InvalidMarginCalculation) + } + } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 36dd5f7aa3..14e9119e27 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -691,8 +691,6 @@ impl User { spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, margin_requirement_type: MarginRequirementType, - user_stats: &mut UserStats, - now: i64, isolated_perp_position_market_index: u16, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; @@ -707,20 +705,20 @@ impl User { context, )?; - if calculation.margin_requirement > 0 || calculation.get_num_of_liabilities()? > 0 { - validate!( - calculation.all_liability_oracles_valid, - ErrorCode::InvalidOracle, - "User attempting to withdraw with outstanding liabilities when an oracle is invalid" - )?; - } + let isolated_position_margin_calculation = calculation.get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; validate!( - calculation.meets_margin_requirement(), + calculation.all_liability_oracles_valid, + ErrorCode::InvalidOracle, + "User attempting to withdraw with outstanding liabilities when an oracle is invalid" + )?; + + validate!( + isolated_position_margin_calculation.meets_margin_requirement(), ErrorCode::InsufficientCollateral, "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - calculation.total_collateral, - calculation.margin_requirement + isolated_position_margin_calculation.total_collateral, + isolated_position_margin_calculation.margin_requirement )?; Ok(true) From 0d564880a80f6b7547c21e7ee820d1b1f2860cc7 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 6 Aug 2025 10:30:01 -0400 Subject: [PATCH 20/31] rm liquidation mode changing context --- programs/drift/src/controller/liquidation.rs | 34 +++++++------------- programs/drift/src/state/liquidation_mode.rs | 10 ------ 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 5a383254e7..95e77b0599 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -147,7 +147,8 @@ pub fn liquidate_perp( perp_market_map, spot_market_map, oracle_map, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; @@ -226,14 +227,14 @@ pub fn liquidate_perp( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -548,7 +549,6 @@ pub fn liquidate_perp( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); @@ -777,13 +777,13 @@ pub fn liquidate_perp_with_fill( now, )?; - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { @@ -851,14 +851,14 @@ pub fn liquidate_perp_with_fill( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -1103,7 +1103,6 @@ pub fn liquidate_perp_with_fill( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; @@ -1673,7 +1672,6 @@ pub fn liquidate_spot( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - MarginContext::liquidation(liquidation_margin_buffer_ratio) )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2246,7 +2244,6 @@ pub fn liquidate_spot_with_swap_end( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - MarginContext::liquidation(liquidation_margin_buffer_ratio) )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2707,8 +2704,6 @@ pub fn liquidate_borrow_for_perp_pnl( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -2945,13 +2940,12 @@ pub fn liquidate_perp_pnl_for_deposit( ) }; - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { @@ -2989,14 +2983,13 @@ pub fn liquidate_perp_pnl_for_deposit( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -3194,7 +3187,6 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; liquidation_mode.increment_free_margin(user, margin_freed_from_liability); @@ -3309,7 +3301,6 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; - let margin_context = liquidation_mode.get_margin_context(0)?; let MarginCalculation { margin_requirement, total_collateral, @@ -3319,7 +3310,7 @@ pub fn resolve_perp_bankruptcy( perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::standard(MarginRequirementType::Maintenance), )?; // spot market's insurance fund draw attempt here (before social loss) @@ -3632,7 +3623,6 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, - margin_context: MarginContext, ) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( @@ -3640,7 +3630,7 @@ pub fn calculate_margin_freed( perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; let new_margin_shortage = margin_calculation_after.margin_shortage()?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index edf8b99c7c..8a950f3404 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -5,8 +5,6 @@ use crate::{controller::{spot_balance::update_spot_balances, spot_position::upda use super::{perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { - fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult; - fn user_is_being_liquidated(&self, user: &User) -> DriftResult; fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; @@ -62,10 +60,6 @@ impl CrossMarginLiquidatePerpMode { } impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { - fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) - } - fn user_is_being_liquidated(&self, user: &User) -> DriftResult { Ok(user.is_being_liquidated()) } @@ -186,10 +180,6 @@ impl IsolatedLiquidatePerpMode { } impl LiquidatePerpMode for IsolatedLiquidatePerpMode { - fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) - } - fn user_is_being_liquidated(&self, user: &User) -> DriftResult { user.is_isolated_position_being_liquidated(self.market_index) } From 584337bf85c0c2af827164779f0935e856af9c10 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 7 Aug 2025 12:52:44 -0400 Subject: [PATCH 21/31] handle liquidation id and bit flags --- programs/drift/src/controller/liquidation.rs | 7 ++++ programs/drift/src/state/events.rs | 6 +++ programs/drift/src/state/liquidation_mode.rs | 12 +++++- programs/drift/src/state/user.rs | 29 +++++++++++-- programs/drift/src/state/user/tests.rs | 43 ++++++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 95e77b0599..9364dbfeaf 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -263,6 +263,7 @@ pub fn liquidate_perp( lp_shares: 0, ..LiquidatePerpRecord::default() }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -699,6 +700,7 @@ pub fn liquidate_perp( liquidator_fee: liquidator_fee.abs().cast()?, if_fee: if_fee.abs().cast()?, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -887,6 +889,7 @@ pub fn liquidate_perp_with_fill( lp_shares: 0, ..LiquidatePerpRecord::default() }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -1143,6 +1146,7 @@ pub fn liquidate_perp_with_fill( liquidator_fee: 0, if_fee: if_fee.abs().cast()?, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -3025,6 +3029,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer: 0, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -3229,6 +3234,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -3456,6 +3462,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 8f93fae37e..9d4fd22fca 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -425,6 +425,7 @@ pub struct LiquidationRecord { pub liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord, pub perp_bankruptcy: PerpBankruptcyRecord, pub spot_bankruptcy: SpotBankruptcyRecord, + pub bit_flags: u8, } #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Eq, Default)] @@ -506,6 +507,11 @@ pub struct SpotBankruptcyRecord { pub cumulative_deposit_interest_delta: u128, } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum LiquidationBitFlag { + IsolatedPosition = 0b00000001, +} + #[event] #[derive(Default)] pub struct SettlePnlRecord { diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 8a950f3404..f0d93513c3 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -2,7 +2,7 @@ use solana_program::msg; use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers}, state::margin_calculation::{MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; -use super::{perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; +use super::{events::LiquidationBitFlag, perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult; @@ -30,6 +30,8 @@ pub trait LiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + fn get_event_bit_flags(&self) -> u8; + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()>; fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult; @@ -109,6 +111,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_bankruptcy()) } + fn get_event_bit_flags(&self) -> u8 { + 0 + } + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { if user.get_spot_position(asset_market_index).is_err() { msg!( @@ -223,6 +229,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_bankruptcy(self.market_index) } + fn get_event_bit_flags(&self) -> u8 { + LiquidationBitFlag::IsolatedPosition as u8 + } + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { validate!( asset_market_index == QUOTE_SPOT_MARKET_INDEX, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 14e9119e27..326e48bcbc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -377,7 +377,15 @@ impl User { self.add_user_status(UserStatus::BeingLiquidated); self.liquidation_margin_freed = 0; self.last_active_slot = slot; - Ok(get_then_update_id!(self, next_liquidation_id)) + + + let liquidation_id = if self.any_isolated_position_being_liquidated() { + self.next_liquidation_id.safe_sub(1)? + } else { + get_then_update_id!(self, next_liquidation_id) + }; + + Ok(liquidation_id) } pub fn exit_liquidation(&mut self) { @@ -397,17 +405,26 @@ impl User { self.liquidation_margin_freed = 0; } + fn any_isolated_position_being_liquidated(&self) -> bool { + self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) + } + pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { - // todo figure out liquidation id if self.is_isolated_position_being_liquidated(perp_market_index)? { return self.next_liquidation_id.safe_sub(1); } + let liquidation_id = if self.is_being_liquidated() || self.any_isolated_position_being_liquidated() { + self.next_liquidation_id.safe_sub(1)? + } else { + get_then_update_id!(self, next_liquidation_id) + }; + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; - Ok(get_then_update_id!(self, next_liquidation_id)) + Ok(liquidation_id) } pub fn exit_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { @@ -418,7 +435,7 @@ impl User { pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0) + Ok(perp_position.is_isolated_position_being_liquidated()) } pub fn enter_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { @@ -1240,6 +1257,10 @@ impl PerpPosition { pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { get_token_amount(self.isolated_position_scaled_balance as u128, spot_market, &SpotBalanceType::Deposit) } + + pub fn is_isolated_position_being_liquidated(&self) -> bool { + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 + } } impl SpotBalance for PerpPosition { diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 57a8654086..94312c9e63 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2309,3 +2309,46 @@ mod update_referrer_status { assert_eq!(user_stats.referrer_status, 1); } } + +mod next_liquidation_id { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + user.next_liquidation_id = 1; + let isolated_position = PerpPosition { + market_index: 1, + position_flag: PositionFlag::IsolatedPosition as u8, + base_asset_amount: 1, + ..PerpPosition::default() + }; + user.perp_positions[0] = isolated_position; + let isolated_position_2 = PerpPosition { + market_index: 2, + position_flag: PositionFlag::IsolatedPosition as u8, + base_asset_amount: 1, + ..PerpPosition::default() + }; + user.perp_positions[1] = isolated_position_2; + + let liquidation_id = user.enter_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 1); + + let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 1); + + user.exit_isolated_position_liquidation(1).unwrap(); + + user.exit_liquidation(); + + let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 2); + + let liquidation_id = user.enter_isolated_position_liquidation(2).unwrap(); + assert_eq!(liquidation_id, 2); + + let liquidation_id = user.enter_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 2); + } +} \ No newline at end of file From 15c05eeea188c93eb5a48f584cab8951e8aedb38 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 7 Aug 2025 17:42:14 -0400 Subject: [PATCH 22/31] more liquidation changes --- programs/drift/src/controller/liquidation.rs | 87 ++++++++++--------- programs/drift/src/math/liquidation.rs | 4 +- programs/drift/src/state/liquidation_mode.rs | 43 ++++++--- .../drift/src/state/margin_calculation.rs | 55 +++++++----- 4 files changed, 114 insertions(+), 75 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9364dbfeaf..63c488d9a7 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -152,10 +152,10 @@ pub fn liquidate_perp( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && margin_calculation.meets_margin_requirement() { + if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user_is_being_liquidated && margin_calculation.can_exit_liquidation()? { + } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -245,15 +245,16 @@ pub fn liquidate_perp( .cast::()?; liquidation_mode.increment_free_margin(user, margin_freed); - if intermediate_margin_calculation.can_exit_liquidation()? { + if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -263,7 +264,7 @@ pub fn liquidate_perp( lp_shares: 0, ..LiquidatePerpRecord::default() }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -677,14 +678,15 @@ pub fn liquidate_perp( }; emit!(fill_record); + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -700,7 +702,7 @@ pub fn liquidate_perp( liquidator_fee: liquidator_fee.abs().cast()?, if_fee: if_fee.abs().cast()?, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -788,10 +790,11 @@ pub fn liquidate_perp_with_fill( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; + if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { + } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -871,15 +874,16 @@ pub fn liquidate_perp_with_fill( .cast::()?; liquidation_mode.increment_free_margin(&mut user, margin_freed); - if intermediate_margin_calculation.can_exit_liquidation()? { + if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -889,11 +893,11 @@ pub fn liquidate_perp_with_fill( lp_shares: 0, ..LiquidatePerpRecord::default() }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); - user.exit_liquidation(); + liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -1123,14 +1127,15 @@ pub fn liquidate_perp_with_fill( existing_direction, )?; + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerp, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -1146,7 +1151,7 @@ pub fn liquidate_perp_with_fill( liquidator_fee: 0, if_fee: if_fee.abs().cast()?, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -1390,7 +1395,7 @@ pub fn liquidate_spot( if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { + } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { user.exit_liquidation(); return Ok(()); } @@ -1437,7 +1442,7 @@ pub fn liquidate_spot( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1921,7 +1926,7 @@ pub fn liquidate_spot_with_swap_begin( if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { + } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); } @@ -1991,7 +1996,7 @@ pub fn liquidate_spot_with_swap_begin( }); // must throw error to stop swap - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { return Err(ErrorCode::InvalidLiquidation); } @@ -2253,7 +2258,7 @@ pub fn liquidate_spot_with_swap_end( margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; - if margin_calulcation_after.can_exit_liquidation()? { + if margin_calulcation_after.cross_margin_can_exit_liquidation()? { user.exit_liquidation(); } else if is_user_bankrupt(user) { user.enter_bankruptcy(); @@ -2493,7 +2498,7 @@ pub fn liquidate_borrow_for_perp_pnl( if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { + } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { user.exit_liquidation(); return Ok(()); } @@ -2536,7 +2541,7 @@ pub fn liquidate_borrow_for_perp_pnl( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; @@ -2952,10 +2957,11 @@ pub fn liquidate_perp_pnl_for_deposit( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; + if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { + } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -3004,20 +3010,21 @@ pub fn liquidate_perp_pnl_for_deposit( .cast::()?; liquidation_mode.increment_free_margin(user, margin_freed); - let exiting_liq_territory = intermediate_margin_calculation.can_exit_liquidation()?; + let exiting_liq_territory = liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; if exiting_liq_territory || is_contract_tier_violation { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerpPnlForDeposit, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -3029,7 +3036,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer: 0, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -3216,14 +3223,15 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map.get_price_data(&market.oracle_id())?.price }; + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::LiquidatePerpPnlForDeposit, user: *user_key, liquidator: *liquidator_key, - margin_requirement: margin_calculation.margin_requirement, - total_collateral: margin_calculation.total_collateral, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3234,7 +3242,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -3307,11 +3315,7 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; - let MarginCalculation { - margin_requirement, - total_collateral, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, @@ -3445,6 +3449,7 @@ pub fn resolve_perp_bankruptcy( let liquidation_id = user.next_liquidation_id.safe_sub(1)?; + let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3462,7 +3467,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index cf425bade7..d58da27314 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -213,7 +213,7 @@ pub fn is_user_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; Ok(is_being_liquidated) } @@ -281,7 +281,7 @@ pub fn is_isolated_position_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index f0d93513c3..1fd5a4d816 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -1,12 +1,16 @@ use solana_program::msg; -use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers}, state::margin_calculation::{MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; +use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap}, state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; use super::{events::LiquidationBitFlag, perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult; + fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult; + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult; + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; fn get_cancel_orders_params(&self) -> (Option, Option, bool); @@ -20,7 +24,7 @@ pub trait LiquidatePerpMode { liquidation_duration: u128, ) -> DriftResult; - fn increment_free_margin(&self, user: &mut User, amount: u64); + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()>; fn is_user_bankrupt(&self, user: &User) -> DriftResult; @@ -30,7 +34,7 @@ pub trait LiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; - fn get_event_bit_flags(&self) -> u8; + fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)>; fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()>; @@ -66,6 +70,14 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.is_being_liquidated()) } + fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + Ok(margin_calculation.cross_margin_meets_margin_requirement()) + } + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { + Ok(margin_calculation.cross_margin_can_exit_liquidation()?) + } + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { Ok(user.exit_liquidation()) } @@ -91,8 +103,8 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { ) } - fn increment_free_margin(&self, user: &mut User, amount: u64) { - user.increment_margin_freed(amount); + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()> { + user.increment_margin_freed(amount) } fn is_user_bankrupt(&self, user: &User) -> DriftResult { @@ -111,8 +123,8 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_bankruptcy()) } - fn get_event_bit_flags(&self) -> u8 { - 0 + fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { + Ok((margin_calculation.margin_requirement, margin_calculation.total_collateral, 0)) } fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { @@ -190,6 +202,14 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.is_isolated_position_being_liquidated(self.market_index) } + fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_position_meets_margin_requirement(self.market_index) + } + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_position_can_exit_liquidation(self.market_index) + } + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { user.exit_isolated_position_liquidation(self.market_index) } @@ -209,8 +229,8 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { Ok(LIQUIDATION_PCT_PRECISION) } - fn increment_free_margin(&self, user: &mut User, amount: u64) { - return; + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()> { + Ok(()) } fn is_user_bankrupt(&self, user: &User) -> DriftResult { @@ -229,8 +249,9 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_bankruptcy(self.market_index) } - fn get_event_bit_flags(&self) -> u8 { - LiquidationBitFlag::IsolatedPosition as u8 + fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { + let isolated_position_margin_calculation = margin_calculation.isolated_position_margin_calculation.get(&self.market_index).safe_unwrap()?; + Ok((isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral, LiquidationBitFlag::IsolatedPosition as u8)) } fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 955b4da458..d79eb0b030 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -5,6 +5,7 @@ use crate::math::casting::Cast; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; use crate::math::margin::MarginRequirementType; use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_balance::get_strict_token_value; use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::PerpMarket; @@ -182,7 +183,6 @@ pub struct MarginCalculation { pub total_spot_liability_value: u128, pub total_perp_liability_value: u128, pub total_perp_pnl: i128, - pub open_orders_margin_requirement: u128, tracked_market_margin_requirement: u128, pub fuel_deposits: u32, pub fuel_borrows: u32, @@ -231,7 +231,6 @@ impl MarginCalculation { total_spot_liability_value: 0, total_perp_liability_value: 0, total_perp_pnl: 0, - open_orders_margin_requirement: 0, tracked_market_margin_requirement: 0, fuel_deposits: 0, fuel_borrows: 0, @@ -315,13 +314,6 @@ impl MarginCalculation { Ok(()) } - pub fn add_open_orders_margin_requirement(&mut self, margin_requirement: u128) -> DriftResult { - self.open_orders_margin_requirement = self - .open_orders_margin_requirement - .safe_add(margin_requirement)?; - Ok(()) - } - pub fn add_spot_liability(&mut self) -> DriftResult { self.num_spot_liabilities = self.num_spot_liabilities.safe_add(1)?; Ok(()) @@ -400,13 +392,13 @@ impl MarginCalculation { } pub fn meets_margin_requirement(&self) -> bool { - let cross_margin_meets_margin_requirement = self.total_collateral >= self.margin_requirement as i128; + let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement(); if !cross_margin_meets_margin_requirement { return false; } - for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement() { return false; } @@ -416,13 +408,13 @@ impl MarginCalculation { } pub fn meets_margin_requirement_with_buffer(&self) -> bool { - let cross_margin_meets_margin_requirement = self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128; + let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement_with_buffer(); if !cross_margin_meets_margin_requirement { return false; } - for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { return false; } @@ -438,21 +430,42 @@ impl MarginCalculation { } } - pub fn positions_meets_margin_requirement(&self) -> DriftResult { - Ok(self.total_collateral - >= self - .margin_requirement - .safe_sub(self.open_orders_margin_requirement)? - .cast::()?) + #[inline(always)] + pub fn cross_margin_meets_margin_requirement(&self) -> bool { + self.total_collateral >= self.margin_requirement as i128 + } + + #[inline(always)] + pub fn cross_margin_meets_margin_requirement_with_buffer(&self) -> bool { + self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + } + + #[inline(always)] + pub fn isolated_position_meets_margin_requirement(&self, market_index: u16) -> DriftResult { + Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement()) + } + + #[inline(always)] + pub fn isolated_position_meets_margin_requirement_with_buffer(&self, market_index: u16) -> DriftResult { + Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) } - pub fn can_exit_liquidation(&self) -> DriftResult { + pub fn cross_margin_can_exit_liquidation(&self) -> DriftResult { if !self.is_liquidation_mode() { msg!("liquidation mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - Ok(self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128) + Ok(self.cross_margin_meets_margin_requirement_with_buffer()) + } + + pub fn isolated_position_can_exit_liquidation(&self, market_index: u16) -> DriftResult { + if !self.is_liquidation_mode() { + msg!("liquidation mode not enabled"); + return Err(ErrorCode::InvalidMarginCalculation); + } + + Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) } pub fn margin_shortage(&self) -> DriftResult { From adc28151d618cd8c6cd632de8d4a8d05ee355f15 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 08:59:11 -0400 Subject: [PATCH 23/31] clean --- programs/drift/src/controller/pnl.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 65d6364be3..d450a2c11e 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -84,8 +84,7 @@ pub fn settle_pnl( // may already be cached let meets_margin_requirement = match meets_margin_requirement { Some(meets_margin_requirement) => meets_margin_requirement, - // TODO check margin for isolate position - _ => meets_settle_pnl_maintenance_margin_requirement( + None => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, From 0de7802976fa2d8ba9930092791817e86168720a Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 09:08:09 -0400 Subject: [PATCH 24/31] fix force cancel orders --- programs/drift/src/controller/orders.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index dc468ac868..89dda68411 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -3191,6 +3191,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() { @@ -3217,6 +3219,10 @@ pub fn force_cancel_orders( continue; } + if cross_margin_meets_initial_margin_requirement { + continue; + } + state.spot_fee_structure.flat_filler_fee } MarketType::Perp => { @@ -3231,9 +3237,15 @@ pub fn force_cancel_orders( continue; } - // TODO: handle force deleting these orders - if user.get_perp_position(market_index)?.is_isolated() { - 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 From 830c7c90841245dc901b80b09cb6005be668b251 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 15:36:46 -0400 Subject: [PATCH 25/31] update validate liquidation --- programs/drift/src/controller/orders.rs | 6 --- programs/drift/src/instructions/user.rs | 1 - programs/drift/src/math/liquidation.rs | 53 +++++++++++-------------- programs/drift/src/state/user.rs | 2 +- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 89dda68411..0607380dd2 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -115,7 +115,6 @@ pub fn place_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - Some(params.market_index), )?; } @@ -1041,7 +1040,6 @@ pub fn fill_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - Some(market_index), ) { Ok(_) => {} Err(_) => { @@ -2953,7 +2951,6 @@ pub fn trigger_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - Some(market_index), )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -3383,7 +3380,6 @@ pub fn place_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - None, )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -3727,7 +3723,6 @@ pub fn fill_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - None, ) { Ok(_) => {} Err(_) => { @@ -5208,7 +5203,6 @@ pub fn trigger_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - None, )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 1d73dcf530..b8eee90d7e 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3793,7 +3793,6 @@ pub fn handle_begin_swap<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, ctx.accounts.state.liquidation_margin_buffer_ratio, - None, )?; let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index d58da27314..e60f6a60a1 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -224,41 +224,34 @@ pub fn validate_user_not_being_liquidated( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, - perp_market_index: Option, ) -> DriftResult { - if !user.is_being_liquidated() { + if !user.is_being_liquidated() && !user.any_isolated_position_being_liquidated() { return Ok(()); } - let is_isolated_perp_market = if let Some(perp_market_index) = perp_market_index { - user.force_get_perp_position_mut(perp_market_index)?.is_isolated() - } else { - false - }; - - let is_still_being_liquidated = if is_isolated_perp_market { - is_isolated_position_being_liquidated( - user, - market_map, - spot_market_map, - oracle_map, - perp_market_index.unwrap(), - liquidation_margin_buffer_ratio, - )? - } else { - is_user_being_liquidated( - user, - market_map, - spot_market_map, - oracle_map, - liquidation_margin_buffer_ratio, - )? - }; + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + )?; - if is_still_being_liquidated { - return Err(ErrorCode::UserIsBeingLiquidated); + if user.is_being_liquidated() { + if margin_calculation.cross_margin_can_exit_liquidation()? { + user.exit_liquidation(); + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } } else { - user.exit_liquidation() + let isolated_positions_being_liquidated = user.perp_positions.iter().filter(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()).map(|position| position.market_index).collect::>(); + for perp_market_index in isolated_positions_being_liquidated { + if margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } + } } Ok(()) @@ -281,7 +274,7 @@ pub fn is_isolated_position_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 326e48bcbc..2c65ad60b3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -405,7 +405,7 @@ impl User { self.liquidation_margin_freed = 0; } - fn any_isolated_position_being_liquidated(&self) -> bool { + pub fn any_isolated_position_being_liquidated(&self) -> bool { self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) } From 5d097399cc9694254f6bc2679a2f216834ce830b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 19:06:15 -0400 Subject: [PATCH 26/31] moar --- programs/drift/src/controller/liquidation.rs | 54 +++++++++------- .../drift/src/controller/liquidation/tests.rs | 4 +- programs/drift/src/math/margin.rs | 4 +- programs/drift/src/math/orders.rs | 15 ++--- programs/drift/src/state/liquidation_mode.rs | 10 +++ .../drift/src/state/margin_calculation.rs | 64 ++++++++++++++----- 6 files changed, 101 insertions(+), 50 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 63c488d9a7..0d7164b986 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -237,13 +237,13 @@ pub fn liquidate_perp( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; + let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - liquidation_mode.increment_free_margin(user, margin_freed); + liquidation_mode.increment_free_margin(user, margin_freed)?; if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; @@ -325,7 +325,7 @@ pub fn liquidate_perp( let margin_ratio_with_buffer = margin_ratio.safe_add(liquidation_margin_buffer_ratio)?; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; let market = perp_market_map.get_ref(&market_index)?; let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; @@ -551,6 +551,7 @@ pub fn liquidate_perp( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); @@ -866,8 +867,8 @@ pub fn liquidate_perp_with_fill( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; + let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -938,7 +939,7 @@ pub fn liquidate_perp_with_fill( let margin_ratio_with_buffer = margin_ratio.safe_add(liquidation_margin_buffer_ratio)?; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; let market = perp_market_map.get_ref(&market_index)?; let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; @@ -1110,6 +1111,7 @@ pub fn liquidate_perp_with_fill( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; @@ -1434,8 +1436,8 @@ pub fn liquidate_spot( .fuel_numerator(user, now), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = margin_calculation.cross_margin_margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -1475,7 +1477,7 @@ pub fn liquidate_spot( margin_calculation.clone() }; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let liability_weight_with_buffer = liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -1681,7 +1683,7 @@ pub fn liquidate_spot( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - + None, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -1964,8 +1966,8 @@ pub fn liquidate_spot_with_swap_begin( .fuel_numerator(user, now), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = margin_calculation.cross_margin_margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -2005,7 +2007,7 @@ pub fn liquidate_spot_with_swap_begin( margin_calculation.clone() }; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let liability_weight_with_buffer = liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -2212,7 +2214,7 @@ pub fn liquidate_spot_with_swap_end( let liquidation_id = user.enter_liquidation(slot)?; let mut margin_freed = 0_u64; - let margin_shortage = margin_calculation.margin_shortage()?; + let margin_shortage = margin_calculation.cross_margin_margin_shortage()?; let if_fee = liability_transfer .cast::()? @@ -2253,6 +2255,7 @@ pub fn liquidate_spot_with_swap_end( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + None, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2533,8 +2536,8 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = margin_calculation.cross_margin_margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -2576,7 +2579,7 @@ pub fn liquidate_borrow_for_perp_pnl( margin_calculation.clone() }; - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = intermediate_margin_calculation.cross_margin_margin_shortage()?; let liability_weight_with_buffer = liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -2713,6 +2716,7 @@ pub fn liquidate_borrow_for_perp_pnl( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + None, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -3002,8 +3006,8 @@ pub fn liquidate_perp_pnl_for_deposit( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let initial_margin_shortage = margin_calculation.margin_shortage()?; - let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; + let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -3069,7 +3073,7 @@ pub fn liquidate_perp_pnl_for_deposit( return Err(ErrorCode::TierViolationLiquidatingPerpPnl); } - let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + let margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; let pnl_liability_weight_plus_buffer = pnl_liability_weight.safe_add(liquidation_margin_buffer_ratio)?; @@ -3199,6 +3203,7 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; liquidation_mode.increment_free_margin(user, margin_freed_from_liability); @@ -3635,6 +3640,7 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, + liquidation_mode: Option<&dyn LiquidatePerpMode>, ) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( @@ -3645,7 +3651,11 @@ pub fn calculate_margin_freed( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let new_margin_shortage = margin_calculation_after.margin_shortage()?; + let new_margin_shortage = if let Some(liquidation_mode) = liquidation_mode { + liquidation_mode.margin_shortage(&margin_calculation_after)? + } else { + margin_calculation_after.cross_margin_margin_shortage()? + }; let margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index e8cf21acde..af47cf0565 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -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); @@ -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); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 4cbbcc5818..6e12af8ca4 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -707,7 +707,7 @@ pub fn meets_place_order_margin_requirement( )?; if !calculation.meets_margin_requirement() { - calculation.print_margin_calculations(); + msg!("margin calculation: {:?}", calculation); return Err(ErrorCode::InsufficientCollateral); } @@ -803,7 +803,7 @@ pub fn calculate_max_withdrawable_amount( return token_amount.cast(); } - let free_collateral = calculation.get_free_collateral()?; + let free_collateral = calculation.get_cross_margin_free_collateral()?; let (numerator_scale, denominator_scale) = if spot_market.decimals > 6 { (10_u128.pow(spot_market.decimals - 6), 1) diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 879bc4e9fe..544480be2e 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -845,14 +845,9 @@ pub fn calculate_max_perp_order_size( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { - let is_isolated_position = user.perp_positions[position_index].is_isolated(); let mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); // calculate initial margin requirement - let MarginCalculation { - margin_requirement, - total_collateral, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, @@ -863,8 +858,12 @@ pub fn calculate_max_perp_order_size( let user_custom_margin_ratio = user.max_margin_ratio; let user_high_leverage_mode = user.is_high_leverage_mode(); - // todo check if this is correct - let free_collateral_before = total_collateral.safe_sub(margin_requirement.cast()?)?; + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + let free_collateral_before = if is_isolated_position { + margin_calculation.get_isolated_position_free_collateral(market_index)?.cast::()? + } else { + margin_calculation.get_cross_margin_free_collateral()?.cast::()? + }; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 1fd5a4d816..28cdb854ec 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -49,6 +49,8 @@ pub trait LiquidatePerpMode { spot_market: &mut SpotMarket, cumulative_deposit_delta: Option, ) -> DriftResult<()>; + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult; } pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { @@ -185,6 +187,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(()) } + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.cross_margin_margin_shortage() + } } pub struct IsolatedLiquidatePerpMode { @@ -302,4 +308,8 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { Ok(()) } + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_position_margin_shortage(self.market_index) + } } \ No newline at end of file diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index d79eb0b030..879e50997b 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -210,6 +210,10 @@ impl IsolatedPositionMarginCalculation { pub fn meets_margin_requirement_with_buffer(&self) -> bool { self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 } + + pub fn margin_shortage(&self) -> DriftResult { + Ok(self.margin_requirement_plus_buffer.cast::()?.safe_sub(self.get_total_collateral_plus_buffer())?.unsigned_abs()) + } } impl MarginCalculation { @@ -280,7 +284,7 @@ impl MarginCalculation { } pub fn add_isolated_position_margin_calculation(&mut self, market_index: u16, deposit_value: i128, pnl: i128, liability_value: u128, margin_requirement: u128) -> DriftResult { - let total_collateral = deposit_value.cast::()?.safe_add(pnl)?; + let total_collateral = deposit_value.safe_add(pnl)?; let total_collateral_buffer = if self.context.margin_buffer > 0 && pnl < 0 { pnl.safe_mul(self.context.margin_buffer.cast::()?)? / MARGIN_PRECISION_I128 @@ -423,13 +427,6 @@ impl MarginCalculation { true } - pub fn print_margin_calculations(&self) { - msg!("cross_margin margin_requirement={}, total_collateral={}", self.margin_requirement, self.total_collateral); - for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { - msg!("isolated_position for market {}: margin_requirement={}, total_collateral={}", market_index, isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral); - } - } - #[inline(always)] pub fn cross_margin_meets_margin_requirement(&self) -> bool { self.total_collateral >= self.margin_requirement as i128 @@ -468,7 +465,7 @@ impl MarginCalculation { Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) } - pub fn margin_shortage(&self) -> DriftResult { + pub fn cross_margin_margin_shortage(&self) -> DriftResult { if self.context.margin_buffer == 0 { msg!("margin buffer mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); @@ -481,29 +478,64 @@ impl MarginCalculation { .unsigned_abs()) } - pub fn tracked_market_margin_shortage(&self, margin_shortage: u128) -> DriftResult { - if self.market_to_track_margin_requirement().is_none() { - msg!("cant call tracked_market_margin_shortage"); + pub fn isolated_position_margin_shortage(&self, market_index: u16) -> DriftResult { + if self.context.margin_buffer == 0 { + msg!("margin buffer mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - if self.margin_requirement == 0 { + self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.margin_shortage() + } + + pub fn tracked_market_margin_shortage(&self, margin_shortage: u128) -> DriftResult { + let MarketIdentifier { + market_type, + market_index, + } = match self.market_to_track_margin_requirement() { + Some(market_to_track) => market_to_track, + None => { + msg!("no market to track margin requirement"); + return Err(ErrorCode::InvalidMarginCalculation); + } + }; + + let margin_requirement = if market_type == MarketType::Perp { + match self.isolated_position_margin_calculation.get(&market_index) { + Some(isolated_position_margin_calculation) => { + isolated_position_margin_calculation.margin_requirement + } + None => { + self.margin_requirement + } + } + } else { + self.margin_requirement + }; + + if margin_requirement == 0 { return Ok(0); } margin_shortage .safe_mul(self.tracked_market_margin_requirement)? - .safe_div(self.margin_requirement) + .safe_div(margin_requirement) } - // todo check every where this is used - pub fn get_free_collateral(&self) -> DriftResult { + pub fn get_cross_margin_free_collateral(&self) -> DriftResult { self.total_collateral .safe_sub(self.margin_requirement.cast::()?)? .max(0) .cast() } + pub fn get_isolated_position_free_collateral(&self, market_index: u16) -> DriftResult { + let isolated_position_margin_calculation = self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?; + isolated_position_margin_calculation.total_collateral + .safe_sub(isolated_position_margin_calculation.margin_requirement.cast::()?)? + .max(0) + .cast() + } + fn market_to_track_margin_requirement(&self) -> Option { if let MarginCalculationMode::Liquidation { market_to_track_margin_requirement: track_margin_requirement, From 7392d3e81d735793f3966790d9fa5813038d2882 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 19:26:07 -0400 Subject: [PATCH 27/31] rename is_being_liquidated --- programs/drift/src/controller/liquidation.rs | 112 +++++++++--------- .../drift/src/controller/liquidation/tests.rs | 12 +- programs/drift/src/controller/orders.rs | 40 +++++-- programs/drift/src/controller/pnl.rs | 4 +- .../drift/src/controller/pnl/delisting.rs | 16 +-- programs/drift/src/instructions/admin.rs | 2 +- programs/drift/src/instructions/user.rs | 58 ++++----- programs/drift/src/math/liquidation.rs | 6 +- programs/drift/src/state/liquidation_mode.rs | 8 +- .../drift/src/state/margin_calculation.rs | 4 + programs/drift/src/state/user.rs | 24 ++-- programs/drift/src/state/user/tests.rs | 32 ++--- programs/drift/src/validation/user.rs | 4 +- 13 files changed, 172 insertions(+), 150 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 0d7164b986..05cc104eca 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -105,7 +105,7 @@ pub fn liquidate_perp( )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -178,7 +178,7 @@ pub fn liquidate_perp( e })?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -255,7 +255,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -688,7 +688,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -752,7 +752,7 @@ pub fn liquidate_perp_with_fill( )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -808,7 +808,7 @@ pub fn liquidate_perp_with_fill( e })?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -885,7 +885,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1138,7 +1138,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1183,13 +1183,13 @@ pub fn liquidate_spot( let liquidation_duration = state.liquidation_duration as u128; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -1394,15 +1394,15 @@ pub fn liquidate_spot( now, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + user.exit_cross_margin_liquidation(); return Ok(()); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let canceled_order_ids = orders::cancel_orders( @@ -1453,7 +1453,7 @@ pub fn liquidate_spot( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_spot: LiquidateSpotRecord { @@ -1468,7 +1468,7 @@ pub fn liquidate_spot( ..LiquidationRecord::default() }); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); return Ok(()); } @@ -1689,9 +1689,9 @@ pub fn liquidate_spot( user.increment_margin_freed(margin_freed_from_liability)?; if liability_transfer >= liability_transfer_to_cover_margin_shortage { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); } let liq_margin_context = MarginContext::standard(MarginRequirementType::Initial) @@ -1726,7 +1726,7 @@ pub fn liquidate_spot( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_spot: LiquidateSpotRecord { asset_market_index, @@ -1765,13 +1765,13 @@ pub fn liquidate_spot_with_swap_begin( let liquidation_duration = state.liquidation_duration as u128; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -1925,15 +1925,15 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let canceled_order_ids = orders::cancel_orders( user, @@ -1982,7 +1982,7 @@ pub fn liquidate_spot_with_swap_begin( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_spot: LiquidateSpotRecord { @@ -2211,7 +2211,7 @@ pub fn liquidate_spot_with_swap_end( now, )?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let margin_shortage = margin_calculation.cross_margin_margin_shortage()?; @@ -2262,9 +2262,9 @@ pub fn liquidate_spot_with_swap_end( user.increment_margin_freed(margin_freed_from_liability)?; if margin_calulcation_after.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); } emit!(LiquidationRecord { @@ -2275,7 +2275,7 @@ pub fn liquidate_spot_with_swap_end( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_spot: LiquidateSpotRecord { asset_market_index, @@ -2315,13 +2315,13 @@ pub fn liquidate_borrow_for_perp_pnl( // blocks borrows where oracle is deemed invalid validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2498,15 +2498,15 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + user.exit_cross_margin_liquidation(); return Ok(()); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let canceled_order_ids = orders::cancel_orders( @@ -2556,7 +2556,7 @@ pub fn liquidate_borrow_for_perp_pnl( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_borrow_for_perp_pnl: LiquidateBorrowForPerpPnlRecord { @@ -2570,7 +2570,7 @@ pub fn liquidate_borrow_for_perp_pnl( ..LiquidationRecord::default() }); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); return Ok(()); } @@ -2722,9 +2722,9 @@ pub fn liquidate_borrow_for_perp_pnl( user.increment_margin_freed(margin_freed_from_liability)?; if liability_transfer >= liability_transfer_to_cover_margin_shortage { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); } let liquidator_meets_initial_margin_requirement = @@ -2749,7 +2749,7 @@ pub fn liquidate_borrow_for_perp_pnl( liquidator: *liquidator_key, margin_requirement: margin_calculation.margin_requirement, total_collateral: margin_calculation.total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_borrow_for_perp_pnl: LiquidateBorrowForPerpPnlRecord { perp_market_index, @@ -2797,7 +2797,7 @@ pub fn liquidate_perp_pnl_for_deposit( )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2970,7 +2970,7 @@ pub fn liquidate_perp_pnl_for_deposit( return Ok(()); } - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); @@ -3029,7 +3029,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3237,7 +3237,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { perp_market_index, @@ -3279,13 +3279,13 @@ pub fn resolve_perp_bankruptcy( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_cross_margin_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -3491,24 +3491,24 @@ pub fn resolve_spot_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - if !user.is_bankrupt() && is_user_bankrupt(user) { - user.enter_bankruptcy(); + if !user.is_cross_margin_bankrupt() && is_user_bankrupt(user) { + user.enter_cross_margin_bankruptcy(); } validate!( - user.is_bankrupt(), + user.is_cross_margin_bankrupt(), ErrorCode::UserNotBankrupt, "user not bankrupt", )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_cross_margin_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -3607,7 +3607,7 @@ pub fn resolve_spot_bankruptcy( // exit bankruptcy if !is_user_bankrupt(user) { - user.exit_bankruptcy(); + user.exit_cross_margin_bankruptcy(); } let liquidation_id = user.next_liquidation_id.safe_sub(1)?; @@ -3673,13 +3673,13 @@ pub fn set_user_status_to_being_liquidated( state: &State, ) -> DriftResult { validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; validate!( - !user.is_being_liquidated(), + !user.is_cross_margin_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "user is already being liquidated", )?; @@ -3694,8 +3694,8 @@ pub fn set_user_status_to_being_liquidated( )?; // todo handle this - if !user.is_being_liquidated() && !margin_calculation.meets_margin_requirement() { - user.enter_liquidation(slot)?; + if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_margin_requirement() { + user.enter_cross_margin_liquidation(slot)?; } let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index af47cf0565..cc815592f3 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -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); } @@ -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)) @@ -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); @@ -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( @@ -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] @@ -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(); diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 0607380dd2..ca09018b8c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -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 { @@ -1028,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)); } @@ -1479,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; } @@ -1945,10 +1945,17 @@ 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); } @@ -2005,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); } @@ -2953,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( @@ -3171,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, @@ -3382,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( @@ -3712,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); } @@ -4020,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; } @@ -5205,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( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index d450a2c11e..ea6df2ed68 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -59,7 +59,7 @@ pub fn settle_pnl( meets_margin_requirement: Option, 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()?; @@ -346,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)?) diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index 67fd12152f..f781b93d1e 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2382,8 +2382,8 @@ pub mod delisting_test { let mut shorter_user_stats = UserStats::default(); let mut liq_user_stats = UserStats::default(); - assert_eq!(shorter.is_being_liquidated(), false); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), false); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); let state = State { liquidation_margin_buffer_ratio: 10, ..Default::default() @@ -2407,8 +2407,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); { let market = market_map.get_ref_mut(&0).unwrap(); @@ -2489,8 +2489,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); { let mut market = market_map.get_ref_mut(&0).unwrap(); @@ -2580,8 +2580,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), true); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), true); { let market = market_map.get_ref_mut(&0).unwrap(); diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 669a6d95b2..00e0e645fd 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4612,7 +4612,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index b8eee90d7e..2fa326edc1 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -532,7 +532,7 @@ pub fn handle_deposit<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; @@ -614,7 +614,7 @@ pub fn handle_deposit<'c: 'info, 'info>( } drop(spot_market); - if user.is_being_liquidated() { + if user.is_cross_margin_being_liquidated() { // try to update liquidation status if user is was already being liq'd let is_being_liquidated = is_user_being_liquidated( user, @@ -625,7 +625,7 @@ pub fn handle_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } @@ -712,7 +712,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market_is_reduce_only = { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; @@ -791,8 +791,8 @@ pub fn handle_withdraw<'c: 'info, 'info>( validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; - if user.is_being_liquidated() { - user.exit_liquidation(); + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); } user.update_last_active_slot(slot); @@ -882,13 +882,13 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_bankrupt(), + !to_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_bankrupt(), + !from_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -970,8 +970,8 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( &mut oracle_map, )?; - if from_user.is_being_liquidated() { - from_user.exit_liquidation(); + if from_user.is_cross_margin_being_liquidated() { + from_user.exit_cross_margin_liquidation(); } from_user.update_last_active_slot(slot); @@ -1104,12 +1104,12 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( let clock = Clock::get()?; validate!( - !to_user.is_bankrupt(), + !to_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_bankrupt(), + !from_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1455,12 +1455,12 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( to_user.update_last_active_slot(slot); - if from_user.is_being_liquidated() { - from_user.exit_liquidation(); + if from_user.is_cross_margin_being_liquidated() { + from_user.exit_cross_margin_liquidation(); } - if to_user.is_being_liquidated() { - to_user.exit_liquidation(); + if to_user.is_cross_margin_being_liquidated() { + to_user.exit_cross_margin_liquidation(); } let deposit_from_spot_market = spot_market_map.get_ref(&deposit_from_market_index)?; @@ -1577,13 +1577,13 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_bankrupt(), + !to_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_bankrupt(), + !from_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1938,7 +1938,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let perp_market = perp_market_map.get_ref(&perp_market_index)?; @@ -2090,7 +2090,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt" )?; @@ -2176,8 +2176,8 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( &mut oracle_map, )?; - if user.is_being_liquidated() { - user.exit_liquidation(); + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); } if user.is_isolated_position_being_liquidated(perp_market_index)? { @@ -2239,7 +2239,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( user.exit_isolated_position_liquidation(perp_market_index)?; } - if user.is_being_liquidated() { + if user.is_cross_margin_being_liquidated() { // try to update liquidation status if user is was already being liq'd let is_being_liquidated = is_user_being_liquidated( user, @@ -2250,7 +2250,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } } @@ -2300,7 +2300,7 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; { let perp_market = &perp_market_map.get_ref(&perp_market_index)?; @@ -3535,7 +3535,7 @@ pub fn handle_update_user_reduce_only( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_reduce_only_status(reduce_only)?; Ok(()) @@ -3548,7 +3548,7 @@ pub fn handle_update_user_advanced_lp( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_advanced_lp_status(advanced_lp)?; Ok(()) @@ -3561,7 +3561,7 @@ pub fn handle_update_user_protected_maker_orders( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; validate!( protected_maker_orders != user.is_protected_maker(), @@ -3785,7 +3785,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( let mut user = load_mut!(&ctx.accounts.user)?; let delegate_is_signer = user.delegate == ctx.accounts.authority.key(); - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; math::liquidation::validate_user_not_being_liquidated( &mut user, diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index e60f6a60a1..e0c28ebfa6 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -225,7 +225,7 @@ pub fn validate_user_not_being_liquidated( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, ) -> DriftResult { - if !user.is_being_liquidated() && !user.any_isolated_position_being_liquidated() { + if !user.is_being_liquidated() { return Ok(()); } @@ -237,9 +237,9 @@ pub fn validate_user_not_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if user.is_being_liquidated() { + if user.is_cross_margin_being_liquidated() { if margin_calculation.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else { return Err(ErrorCode::UserIsBeingLiquidated); } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 28cdb854ec..bb3c2c54d2 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -69,7 +69,7 @@ impl CrossMarginLiquidatePerpMode { impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult { - Ok(user.is_being_liquidated()) + Ok(user.is_cross_margin_being_liquidated()) } fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { @@ -81,7 +81,7 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { - Ok(user.exit_liquidation()) + Ok(user.exit_cross_margin_liquidation()) } fn get_cancel_orders_params(&self) -> (Option, Option, bool) { @@ -118,11 +118,11 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - Ok(user.enter_bankruptcy()) + Ok(user.enter_cross_margin_bankruptcy()) } fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - Ok(user.exit_bankruptcy()) + Ok(user.exit_cross_margin_bankruptcy()) } fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 879e50997b..4db918b828 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -646,4 +646,8 @@ impl MarginCalculation { Err(ErrorCode::InvalidMarginCalculation) } } + + pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { + self.isolated_position_margin_calculation.contains_key(&market_index) + } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 2c65ad60b3..96ecd127bf 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -135,10 +135,14 @@ pub struct User { impl User { pub fn is_being_liquidated(&self) -> bool { + self.is_cross_margin_being_liquidated() || self.has_isolated_position_being_liquidated() + } + + pub fn is_cross_margin_being_liquidated(&self) -> bool { self.status & (UserStatus::BeingLiquidated as u8 | UserStatus::Bankrupt as u8) > 0 } - pub fn is_bankrupt(&self) -> bool { + pub fn is_cross_margin_bankrupt(&self) -> bool { self.status & (UserStatus::Bankrupt as u8) > 0 } @@ -369,8 +373,8 @@ impl User { Ok(()) } - pub fn enter_liquidation(&mut self, slot: u64) -> DriftResult { - if self.is_being_liquidated() { + pub fn enter_cross_margin_liquidation(&mut self, slot: u64) -> DriftResult { + if self.is_cross_margin_being_liquidated() { return self.next_liquidation_id.safe_sub(1); } @@ -379,7 +383,7 @@ impl User { self.last_active_slot = slot; - let liquidation_id = if self.any_isolated_position_being_liquidated() { + let liquidation_id = if self.has_isolated_position_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -388,24 +392,24 @@ impl User { Ok(liquidation_id) } - pub fn exit_liquidation(&mut self) { + pub fn exit_cross_margin_liquidation(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.remove_user_status(UserStatus::Bankrupt); self.liquidation_margin_freed = 0; } - pub fn enter_bankruptcy(&mut self) { + pub fn enter_cross_margin_bankruptcy(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.add_user_status(UserStatus::Bankrupt); } - pub fn exit_bankruptcy(&mut self) { + pub fn exit_cross_margin_bankruptcy(&mut self) { self.remove_user_status(UserStatus::BeingLiquidated); self.remove_user_status(UserStatus::Bankrupt); self.liquidation_margin_freed = 0; } - pub fn any_isolated_position_being_liquidated(&self) -> bool { + pub fn has_isolated_position_being_liquidated(&self) -> bool { self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) } @@ -414,7 +418,7 @@ impl User { return self.next_liquidation_id.safe_sub(1); } - let liquidation_id = if self.is_being_liquidated() || self.any_isolated_position_being_liquidated() { + let liquidation_id = if self.is_cross_margin_being_liquidated() || self.has_isolated_position_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -462,7 +466,7 @@ impl User { } pub fn update_last_active_slot(&mut self, slot: u64) { - if !self.is_being_liquidated() { + if !self.is_cross_margin_being_liquidated() { self.last_active_slot = slot; } self.idle = false; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 94312c9e63..09484892ab 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -1671,36 +1671,36 @@ mod update_user_status { let mut user = User::default(); assert_eq!(user.status, 0); - user.enter_liquidation(0).unwrap(); + user.enter_cross_margin_liquidation(0).unwrap(); assert_eq!(user.status, UserStatus::BeingLiquidated as u8); - assert!(user.is_being_liquidated()); + assert!(user.is_cross_margin_being_liquidated()); - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); assert_eq!(user.status, UserStatus::Bankrupt as u8); - assert!(user.is_being_liquidated()); - assert!(user.is_bankrupt()); + assert!(user.is_cross_margin_being_liquidated()); + assert!(user.is_cross_margin_bankrupt()); let mut user = User { status: UserStatus::ReduceOnly as u8, ..User::default() }; - user.enter_liquidation(0).unwrap(); + user.enter_cross_margin_liquidation(0).unwrap(); - assert!(user.is_being_liquidated()); + assert!(user.is_cross_margin_being_liquidated()); assert!(user.status & UserStatus::ReduceOnly as u8 > 0); - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); - assert!(user.is_being_liquidated()); - assert!(user.is_bankrupt()); + assert!(user.is_cross_margin_being_liquidated()); + assert!(user.is_cross_margin_bankrupt()); assert!(user.status & UserStatus::ReduceOnly as u8 > 0); - user.exit_liquidation(); - assert!(!user.is_being_liquidated()); - assert!(!user.is_bankrupt()); + user.exit_cross_margin_liquidation(); + assert!(!user.is_cross_margin_being_liquidated()); + assert!(!user.is_cross_margin_bankrupt()); assert!(user.status & UserStatus::ReduceOnly as u8 > 0); } } @@ -2332,7 +2332,7 @@ mod next_liquidation_id { }; user.perp_positions[1] = isolated_position_2; - let liquidation_id = user.enter_liquidation(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 1); let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); @@ -2340,7 +2340,7 @@ mod next_liquidation_id { user.exit_isolated_position_liquidation(1).unwrap(); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); @@ -2348,7 +2348,7 @@ mod next_liquidation_id { let liquidation_id = user.enter_isolated_position_liquidation(2).unwrap(); assert_eq!(liquidation_id, 2); - let liquidation_id = user.enter_liquidation(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); } } \ No newline at end of file diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index 3f527fed0f..f19851b35f 100644 --- a/programs/drift/src/validation/user.rs +++ b/programs/drift/src/validation/user.rs @@ -17,7 +17,7 @@ pub fn validate_user_deletion( )?; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserCantBeDeleted, "user bankrupt" )?; @@ -87,7 +87,7 @@ pub fn validate_user_is_idle(user: &User, slot: u64, accelerated: bool) -> Drift )?; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserNotInactive, "user bankrupt" )?; From 26960c8a7e4be44a55f5ab1dc108c28b41cbabbd Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 16:46:18 -0400 Subject: [PATCH 28/31] start adding test --- programs/drift/src/controller/amm/tests.rs | 1 - programs/drift/src/controller/liquidation.rs | 110 ++- .../drift/src/controller/liquidation/tests.rs | 901 ++++++++++++++++++ programs/drift/src/controller/orders.rs | 54 +- programs/drift/src/controller/pnl.rs | 5 +- .../drift/src/controller/pnl/delisting.rs | 2 +- programs/drift/src/controller/position.rs | 14 +- .../drift/src/controller/position/tests.rs | 5 +- programs/drift/src/instructions/keeper.rs | 5 +- programs/drift/src/instructions/user.rs | 39 +- programs/drift/src/lib.rs | 21 +- programs/drift/src/math/bankruptcy.rs | 6 +- programs/drift/src/math/cp_curve/tests.rs | 2 +- programs/drift/src/math/funding.rs | 4 +- programs/drift/src/math/liquidation.rs | 22 +- programs/drift/src/math/margin.rs | 37 +- programs/drift/src/math/orders.rs | 8 +- programs/drift/src/math/position.rs | 5 +- programs/drift/src/state/liquidation_mode.rs | 116 ++- .../drift/src/state/margin_calculation.rs | 106 ++- programs/drift/src/state/user.rs | 54 +- programs/drift/src/state/user/tests.rs | 2 +- 22 files changed, 1324 insertions(+), 195 deletions(-) diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index 5031a75bbc..a2a33fd6d5 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -255,7 +255,6 @@ fn iterative_no_bounds_formualic_k_tests() { assert_eq!(market.amm.total_fee_minus_distributions, 985625029); } - #[test] fn update_pool_balances_test_high_util_borrow() { let mut market = PerpMarket { diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 05cc104eca..07d2ee1203 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,7 +1,9 @@ use std::ops::{Deref, DerefMut}; use crate::msg; -use crate::state::liquidation_mode::{get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode}; +use crate::state::liquidation_mode::{ + get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode, +}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -96,7 +98,7 @@ pub fn liquidate_perp( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; - let liquidation_mode = get_perp_liquidation_mode(&user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; validate!( !liquidation_mode.is_user_bankrupt(&user)?, @@ -152,10 +154,14 @@ pub fn liquidate_perp( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { + if !user_is_being_liquidated + && liquidation_mode.meets_margin_requirements(&margin_calculation)? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -188,7 +194,8 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = liquidation_mode.get_cancel_orders_params(); + let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -238,7 +245,8 @@ pub fn liquidate_perp( )?; let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; - let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -246,7 +254,8 @@ pub fn liquidate_perp( liquidation_mode.increment_free_margin(user, margin_freed)?; if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -679,7 +688,8 @@ pub fn liquidate_perp( }; emit!(fill_record); - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -736,7 +746,7 @@ pub fn liquidate_perp_with_fill( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; - let liquidation_mode = get_perp_liquidation_mode(&user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; validate!( !liquidation_mode.is_user_bankrupt(&user)?, @@ -792,10 +802,14 @@ pub fn liquidate_perp_with_fill( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { + if !user_is_being_liquidated + && liquidation_mode.meets_margin_requirements(&margin_calculation)? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -817,8 +831,9 @@ pub fn liquidate_perp_with_fill( || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); + + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, user_key, @@ -868,7 +883,8 @@ pub fn liquidate_perp_with_fill( )?; let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; - let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -876,7 +892,8 @@ pub fn liquidate_perp_with_fill( liquidation_mode.increment_free_margin(&mut user, margin_freed); if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1129,7 +1146,8 @@ pub fn liquidate_perp_with_fill( existing_direction, )?; - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1397,7 +1415,9 @@ pub fn liquidate_spot( if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.cross_margin_can_exit_liquidation()? + { user.exit_cross_margin_liquidation(); return Ok(()); } @@ -1928,7 +1948,9 @@ pub fn liquidate_spot_with_swap_begin( if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.cross_margin_can_exit_liquidation()? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); } @@ -1948,7 +1970,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, - true + true, )?; // check if user exited liquidation territory @@ -2501,7 +2523,9 @@ pub fn liquidate_borrow_for_perp_pnl( if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.cross_margin_can_exit_liquidation()? + { user.exit_cross_margin_liquidation(); return Ok(()); } @@ -2522,7 +2546,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, - true + true, )?; // check if user exited liquidation territory @@ -2788,7 +2812,7 @@ pub fn liquidate_perp_pnl_for_deposit( // blocked when 1) user deposit oracle is deemed invalid // or 2) user has outstanding liability with higher tier - let liquidation_mode = get_perp_liquidation_mode(&user, perp_market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, perp_market_index)?; validate!( !liquidation_mode.is_user_bankrupt(&user)?, @@ -2962,10 +2986,14 @@ pub fn liquidate_perp_pnl_for_deposit( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { + if !user_is_being_liquidated + && liquidation_mode.meets_margin_requirements(&margin_calculation)? + { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -2973,7 +3001,8 @@ pub fn liquidate_perp_pnl_for_deposit( let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -2990,8 +3019,8 @@ pub fn liquidate_perp_pnl_for_deposit( cancel_orders_is_isolated, )?; - let (safest_tier_spot_liability, safest_tier_perp_liability) = - liquidation_mode.calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; + let (safest_tier_spot_liability, safest_tier_perp_liability) = liquidation_mode + .calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; let is_contract_tier_violation = !(contract_tier.is_as_safe_as(&safest_tier_perp_liability, &safest_tier_spot_liability)); @@ -3007,20 +3036,23 @@ pub fn liquidate_perp_pnl_for_deposit( )?; let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; - let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; liquidation_mode.increment_free_margin(user, margin_freed); - let exiting_liq_territory = liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; + let exiting_liq_territory = + liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; if exiting_liq_territory || is_contract_tier_violation { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3228,7 +3260,8 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map.get_price_data(&market.oracle_id())?.price }; - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3266,9 +3299,11 @@ pub fn resolve_perp_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - let liquidation_mode = get_perp_liquidation_mode(&user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; - if !liquidation_mode.is_user_bankrupt(&user)? && liquidation_mode.should_user_enter_bankruptcy(&user)? { + if !liquidation_mode.is_user_bankrupt(&user)? + && liquidation_mode.should_user_enter_bankruptcy(&user)? + { liquidation_mode.enter_bankruptcy(user)?; } @@ -3454,7 +3489,8 @@ pub fn resolve_perp_bankruptcy( let liquidation_id = user.next_liquidation_id.safe_sub(1)?; - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3698,7 +3734,11 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); + let isolated_position_market_indexes = user + .perp_positions + .iter() + .filter_map(|position| position.is_isolated().then_some(position.market_index)) + .collect::>(); // for market_index in isolated_position_market_indexes { // let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index cc815592f3..4548cbbb33 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -8910,3 +8910,904 @@ mod liquidate_dust_spot_market { assert_eq!(result, Ok(())); } } + +pub mod liquidate_isolated_perp { + use crate::math::constants::ONE_HOUR; + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::liquidate_perp; + use crate::controller::position::PositionDirection; + use crate::create_anchor_account_info; + use crate::error::ErrorCode; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, + LIQUIDATION_FEE_PRECISION, LIQUIDATION_PCT_PRECISION, MARGIN_PRECISION, + MARGIN_PRECISION_U128, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, + QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::liquidation::is_user_being_liquidated; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::math::position::calculate_base_asset_value_with_oracle_price; + use crate::state::margin_calculation::{MarginCalculation, MarginContext}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{MarketStatus, 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::user::{ + MarginMode, Order, OrderStatus, OrderType, PerpPosition, PositionFlag, SpotPosition, User, + UserStats, + }; + use crate::test_utils::*; + use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_liquidation_long_perp() { + 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, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..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, + position_flag: PositionFlag::IsolatedPosition as u8, + ..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, + -51 * QUOTE_PRECISION_I64 + ); + 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, + -99 * QUOTE_PRECISION_I64 + ); + + 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() { + 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, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + 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, + position_flag: PositionFlag::IsolatedPosition as u8, + ..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, + -51 * QUOTE_PRECISION_I64 + ); + 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, + 101 * QUOTE_PRECISION_I64 + ); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + } + + #[test] + pub fn successful_liquidation_to_cover_margin_shortage() { + 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, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: ONE_HOUR, + ..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, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..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: 2 * BASE_PRECISION_I64, + quote_asset_amount: -200 * QUOTE_PRECISION_I64, + quote_entry_amount: -200 * QUOTE_PRECISION_I64, + quote_break_even_amount: -200 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 5 * SPOT_BALANCE_PRECISION_U64, + ..PerpPosition::default() + }), + + ..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: MARGIN_PRECISION / 50, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + 10 * 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, 200000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -23600000); + assert_eq!(user.perp_positions[0].quote_entry_amount, -20000000); + assert_eq!(user.perp_positions[0].quote_break_even_amount, -23600000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(state.liquidation_margin_buffer_ratio), + ) + .unwrap(); + + let isolated_margin_calculation = margin_calculation + .get_isolated_position_margin_calculation(0) + .unwrap(); + let total_collateral = isolated_margin_calculation.total_collateral; + let margin_requirement_plus_buffer = + isolated_margin_calculation.margin_requirement_plus_buffer; + + // user out of liq territory + assert_eq!( + total_collateral.unsigned_abs(), + margin_requirement_plus_buffer + ); + + let oracle_price = oracle_map + .get_price_data(&(oracle_price_key, OracleSource::Pyth)) + .unwrap() + .price; + + let perp_value = calculate_base_asset_value_with_oracle_price( + user.perp_positions[0].base_asset_amount as i128, + oracle_price, + ) + .unwrap(); + + let margin_ratio = total_collateral.unsigned_abs() * MARGIN_PRECISION_U128 / perp_value; + + assert_eq!(margin_ratio, 700); + + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 1800000000); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -178200000); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 1800000) + } + + #[test] + pub fn liquidation_over_multiple_slots_takes_one() { + let now = 1_i64; + let slot = 1_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, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: ONE_HOUR, + ..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, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..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: 10 * BASE_PRECISION_U64, + slot: 0, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 20 * BASE_PRECISION_I64, + quote_asset_amount: -2000 * QUOTE_PRECISION_I64, + quote_entry_amount: -2000 * QUOTE_PRECISION_I64, + quote_break_even_amount: -2000 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: 10 * BASE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 500 * 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: MARGIN_PRECISION / 50, + initial_pct_to_liquidate: (LIQUIDATION_PCT_PRECISION / 10) as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + 20 * 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, 2000000000); + assert_eq!( + user.perp_positions[0].is_isolated_position_being_liquidated(), + false + ); + } + + #[test] + pub fn successful_liquidation_half_of_if_fee() { + 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, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: 3600, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 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 { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: 100 * QUOTE_PRECISION_I64, + quote_entry_amount: 100 * QUOTE_PRECISION_I64, + quote_break_even_amount: 100 * QUOTE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 15 * SPOT_BALANCE_PRECISION_U64 / 10, // $1.5 + ..PerpPosition::default() + }), + ..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(); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + // .5% * 100 * .95 =$0.475 + assert_eq!(market_after.amm.total_liquidation_fee, 475000); + } + + #[test] + pub fn successful_liquidation_portion_of_if_fee() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_hardcoded_pyth_price(23244136, 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, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + funding_period: 3600, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 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 { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -299400000000, + quote_asset_amount: 6959294318, + quote_entry_amount: 6959294318, + quote_break_even_amount: 6959294318, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 113838792 * 1000, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50000 * 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: 200, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + liquidate_perp( + 0, + 300 * 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(); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert!(!user.is_isolated_position_being_liquidated(0).unwrap()); + assert_eq!(market_after.amm.total_liquidation_fee, 41787043); + } +} \ No newline at end of file diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index ca09018b8c..f30722744e 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -11,8 +11,7 @@ use crate::controller::funding::settle_funding_payment; use crate::controller::position; use crate::controller::position::{ add_new_position, decrease_open_bids_and_asks, get_position_index, increase_open_bids_and_asks, - update_position_and_market, update_quote_asset_amount, - PositionDirection, + update_position_and_market, update_quote_asset_amount, PositionDirection, }; use crate::controller::spot_balance::{ update_spot_balances, update_spot_market_cumulative_interest, @@ -522,7 +521,12 @@ pub fn cancel_orders( skip_isolated_positions: bool, ) -> DriftResult> { let mut canceled_order_ids: Vec = vec![]; - let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::>(); + let isolated_position_market_indexes = user + .perp_positions + .iter() + .filter(|position| position.is_isolated()) + .map(|position| position.market_index) + .collect::>(); for order_index in 0..user.orders.len() { if user.orders[order_index].status != OrderStatus::Open { continue; @@ -536,7 +540,9 @@ 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) { + } else if skip_isolated_positions + && isolated_position_market_indexes.contains(&user.orders[order_index].market_index) + { continue; } @@ -1945,11 +1951,20 @@ 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) + 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) + ( + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral, + ) }; msg!( @@ -2012,11 +2027,20 @@ 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) + 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) + ( + maker_margin_calculation.margin_requirement, + maker_margin_calculation.total_collateral, + ) }; msg!( @@ -3202,7 +3226,8 @@ pub fn force_cancel_orders( ErrorCode::SufficientCollateral )?; - let cross_margin_meets_initial_margin_requirement = margin_calc.cross_margin_meets_margin_requirement(); + let cross_margin_meets_initial_margin_requirement = + margin_calc.cross_margin_meets_margin_requirement(); let mut total_fee = 0_u64; @@ -3253,7 +3278,8 @@ pub fn force_cancel_orders( continue; } } else { - let isolated_position_meets_margin_requirement = margin_calc.isolated_position_meets_margin_requirement(market_index)?; + let isolated_position_meets_margin_requirement = + margin_calc.isolated_position_meets_margin_requirement(market_index)?; if isolated_position_meets_margin_requirement { continue; } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index ea6df2ed68..14e4fbd865 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -1,9 +1,6 @@ use crate::controller::amm::{update_pnl_pool_and_user_balance, update_pool_balances}; use crate::controller::funding::settle_funding_payment; -use crate::controller::orders::{ - cancel_orders, - validate_market_within_price_band, -}; +use crate::controller::orders::{cancel_orders, validate_market_within_price_band}; use crate::controller::position::{ get_position_index, update_position_and_market, update_quote_asset_amount, update_quote_asset_and_break_even_amount, update_settled_pnl, PositionDelta, diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index f781b93d1e..70f81a716b 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2601,7 +2601,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, - false + false, ) .unwrap(); diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index fea9ef135a..30150d3c6e 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -95,10 +95,8 @@ pub fn update_position_and_market( let update_type = get_position_update_type(position, delta)?; // Update User - let ( - new_base_asset_amount, - new_quote_asset_amount, - ) = get_new_position_amounts(position, delta)?; + let (new_base_asset_amount, new_quote_asset_amount) = + get_new_position_amounts(position, delta)?; let (new_quote_entry_amount, new_quote_break_even_amount, pnl) = match update_type { PositionUpdateType::Open | PositionUpdateType::Increase => { @@ -475,9 +473,7 @@ pub fn update_quote_asset_amount( return Ok(()); } - if position.quote_asset_amount == 0 - && position.base_asset_amount == 0 - { + if position.quote_asset_amount == 0 && position.base_asset_amount == 0 { market.number_of_users = market.number_of_users.safe_add(1)?; } @@ -485,9 +481,7 @@ pub fn update_quote_asset_amount( market.amm.quote_asset_amount = market.amm.quote_asset_amount.safe_add(delta.cast()?)?; - if position.quote_asset_amount == 0 - && position.base_asset_amount == 0 - { + if position.quote_asset_amount == 0 && position.base_asset_amount == 0 { market.number_of_users = market.number_of_users.saturating_sub(1); } diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index bfe693fe44..7c41bf2591 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -1,9 +1,7 @@ use crate::controller::amm::{ calculate_base_swap_output_with_spread, move_price, recenter_perp_market_amm, swap_base_asset, }; -use crate::controller::position::{ - update_position_and_market, PositionDelta, -}; +use crate::controller::position::{update_position_and_market, PositionDelta}; use crate::controller::repeg::_update_amm; use crate::math::amm::calculate_market_open_bids_asks; @@ -41,7 +39,6 @@ use anchor_lang::Owner; use solana_program::pubkey::Pubkey; use std::str::FromStr; - #[test] fn amm_pool_balance_liq_fees_example() { let perp_market_str = String::from("Ct8MLGv1N/dquEe6RHLCjPXRFs689/VXwfnq/aHEADtX6J/C8GaZXDKZ6iACt2rxmu8p8Fh+gR3ERNNiw5jAdKhvts0jU4yP8/YGAAAAAAAAAAAAAAAAAAEAAAAAAAAAYOoGAAAAAAD08AYAAAAAAFDQ0WcAAAAAU20cou///////////////zqG0jcAAAAAAAAAAAAAAACyy62lmssEAAAAAAAAAAAAAAAAAAAAAACuEBLjOOAUAAAAAAAAAAAAiQqZJDPTFAAAAAAAAAAAANiFEAAAAAAAAAAAAAAAAABEI0dQmUcTAAAAAAAAAAAAxIkaBDObFgAAAAAAAAAAAD4fkf+02RQAAAAAAAAAAABN+wYAAAAAAAAAAAAAAAAAy1BRbfXSFAAAAAAAAAAAAADOOHkhTQcAAAAAAAAAAAAAFBriILP4////////////SMyW3j0AAAAAAAAAAAAAALgVvHwEAAAAAAAAAAAAAAAAADQm9WscAAAAAAAAAAAAURkvFjoAAAAAAAAAAAAAAHIxjo/f/f/////////////TuoG31QEAAAAAAAAAAAAAP8QC+7L9/////////////3SO4oj1AQAAAAAAAAAAAAAAgFcGo5wAAAAAAAAAAAAAzxUAAAAAAADPFQAAAAAAAM8VAAAAAAAAPQwAAAAAAABk1DIXBgEAAAAAAAAAAAAAKqQCt7MAAAAAAAAAAAAAAP0Q55dSAAAAAAAAAAAAAACS+qA0KQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALB5hg2UAAAAAAAAAAAAAAAnMANRAAAAAAAAAAAAAAAAmdj/UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+LAqY3t8UAAAAAAAAAAAAhk/TOI3TFAAAAAAAAAAAAG1uRreN4BQAAAAAAAAAAABkKKeG3tIUAAAAAAAAAAAA8/YGAAAAAAD+/////////2DqBgAAAAAA5OoGAAAAAACi6gYAAAAAAKzxBgAAAAAAMj1zEwAAAABIAgAAAAAAAIy24v//////tMvRZwAAAAAQDgAAAAAAAADKmjsAAAAAZAAAAAAAAAAA8gUqAQAAAAAAAAAAAAAAs3+BskEAAADIfXYRAAAAAIIeqQIAAAAAdb7RZwAAAABxDAAAAAAAAJMMAAAAAAAAUNDRZwAAAAD6AAAA1DAAAIQAAAB9AAAAfgAAAAAAAABkADIAZGQMAQAAAAADAAAAX79DBQAAAABIC9oEAwAAAK3TwZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFdJRi1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADd4BgAAAAAAlCUAAAAAAAAcCgAAAAAAAGQAAABkAAAAqGEAAFDDAADECQAA4gQAAAAAAAAQJwAA2QAAAIgBAAAXAAEAAwAAAAAAAAEBAOgD9AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 2591439bfd..101ccfb84e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2708,14 +2708,13 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let custom_margin_ratio_before = user.max_margin_ratio; user.max_margin_ratio = 0; - let margin_buffer= MARGIN_PRECISION / 100; // 1% buffer + let margin_buffer = MARGIN_PRECISION / 100; // 1% buffer let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(margin_buffer), + MarginContext::standard(MarginRequirementType::Initial).margin_buffer(margin_buffer), )?; let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 2fa326edc1..82eee452b1 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1950,7 +1950,6 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( spot_market_index )?; - let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; @@ -2169,12 +2168,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( now, )?; - validate_spot_margin_trading( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; if user.is_cross_margin_being_liquidated() { user.exit_cross_margin_liquidation(); @@ -2190,7 +2184,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( perp_market_index, state.liquidation_margin_buffer_ratio, )?; - + if !is_being_liquidated { user.exit_isolated_position_liquidation(perp_market_index)?; } @@ -2198,7 +2192,9 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( } else { let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; - let isolated_perp_position_token_amount = user.force_get_isolated_perp_position_mut(perp_market_index)?.get_isolated_position_token_amount(&spot_market)?; + let isolated_perp_position_token_amount = user + .force_get_isolated_perp_position_mut(perp_market_index)? + .get_isolated_position_token_amount(&spot_market)?; validate!( amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, @@ -2248,15 +2244,13 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( &mut oracle_map, state.liquidation_margin_buffer_ratio, )?; - + if !is_being_liquidated { user.exit_cross_margin_liquidation(); } } } - - user.update_last_active_slot(slot); let spot_market = spot_market_map.get_ref(&spot_market_index)?; @@ -2328,9 +2322,11 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( spot_market.get_precision().cast()?, )?; - let isolated_perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + let isolated_perp_position = + user.force_get_isolated_perp_position_mut(perp_market_index)?; - let isolated_position_token_amount = isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + let isolated_position_token_amount = + isolated_perp_position.get_isolated_position_token_amount(spot_market)?; validate!( amount as u128 <= isolated_position_token_amount, @@ -3535,7 +3531,10 @@ pub fn handle_update_user_reduce_only( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!( + !user.is_cross_margin_being_liquidated(), + ErrorCode::LiquidationsOngoing + )?; user.update_reduce_only_status(reduce_only)?; Ok(()) @@ -3548,7 +3547,10 @@ pub fn handle_update_user_advanced_lp( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!( + !user.is_cross_margin_being_liquidated(), + ErrorCode::LiquidationsOngoing + )?; user.update_advanced_lp_status(advanced_lp)?; Ok(()) @@ -3561,7 +3563,10 @@ pub fn handle_update_user_protected_maker_orders( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!( + !user.is_cross_margin_being_liquidated(), + ErrorCode::LiquidationsOngoing + )?; validate!( protected_maker_orders != user.is_protected_maker(), diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 5f172e3043..7edf12a991 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -174,7 +174,12 @@ pub mod drift { perp_market_index: u16, amount: u64, ) -> Result<()> { - handle_deposit_into_isolated_perp_position(ctx, spot_market_index, perp_market_index, amount) + handle_deposit_into_isolated_perp_position( + ctx, + spot_market_index, + perp_market_index, + amount, + ) } pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( @@ -183,7 +188,12 @@ pub mod drift { perp_market_index: u16, amount: i64, ) -> Result<()> { - handle_transfer_isolated_perp_position_deposit(ctx, spot_market_index, perp_market_index, amount) + handle_transfer_isolated_perp_position_deposit( + ctx, + spot_market_index, + perp_market_index, + amount, + ) } pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( @@ -192,7 +202,12 @@ pub mod drift { perp_market_index: u16, amount: u64, ) -> Result<()> { - handle_withdraw_from_isolated_perp_position(ctx, spot_market_index, perp_market_index, amount) + handle_withdraw_from_isolated_perp_position( + ctx, + spot_market_index, + perp_market_index, + amount, + ) } pub fn place_perp_order<'c: 'info, 'info>( diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 7defdea6b3..f8963c61c4 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -42,5 +42,7 @@ pub fn is_user_isolated_position_bankrupt(user: &User, market_index: u16) -> Dri return Ok(false); } - return Ok(perp_position.base_asset_amount == 0 && perp_position.quote_asset_amount < 0 && !perp_position.has_open_order()); -} \ No newline at end of file + return Ok(perp_position.base_asset_amount == 0 + && perp_position.quote_asset_amount < 0 + && !perp_position.has_open_order()); +} diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index 2ae3d2506b..f9100c98cf 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -365,4 +365,4 @@ fn amm_spread_adj_logic() { update_spreads(&mut market, reserve_price).unwrap(); assert_eq!(market.amm.long_spread, 110); assert_eq!(market.amm.short_spread, 110); -} \ No newline at end of file +} diff --git a/programs/drift/src/math/funding.rs b/programs/drift/src/math/funding.rs index 683f62c2cc..c723cb37bb 100644 --- a/programs/drift/src/math/funding.rs +++ b/programs/drift/src/math/funding.rs @@ -27,9 +27,7 @@ pub fn calculate_funding_rate_long_short( ) -> DriftResult<(i128, i128, i128)> { // Calculate the funding payment owed by the net_market_position if funding is not capped // If the net market position owes funding payment, the protocol receives payment - let settled_net_market_position = market - .amm - .base_asset_amount_with_amm; + let settled_net_market_position = market.amm.base_asset_amount_with_amm; let net_market_position_funding_payment = calculate_funding_payment_in_quote_precision(funding_rate, settled_net_market_position)?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index e0c28ebfa6..9f25648c4e 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -244,13 +244,20 @@ pub fn validate_user_not_being_liquidated( return Err(ErrorCode::UserIsBeingLiquidated); } } else { - let isolated_positions_being_liquidated = user.perp_positions.iter().filter(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()).map(|position| position.market_index).collect::>(); + let isolated_positions_being_liquidated = user + .perp_positions + .iter() + .filter(|position| { + position.is_isolated() && position.is_isolated_position_being_liquidated() + }) + .map(|position| position.market_index) + .collect::>(); for perp_market_index in isolated_positions_being_liquidated { - if margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; - } else { - return Err(ErrorCode::UserIsBeingLiquidated); - } + if margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } } } @@ -274,7 +281,8 @@ pub fn is_isolated_position_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; + let is_being_liquidated = + !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 6e12af8ca4..16f75868ae 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -525,20 +525,16 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; - let ( - perp_margin_requirement, - weighted_pnl, - worst_case_liability_value, - base_asset_value, - ) = calculate_perp_position_value_and_pnl( - market_position, - market, - oracle_price_data, - &strict_quote_price, - context.margin_type, - user_custom_margin_ratio, - user_high_leverage_mode, - )?; + let (perp_margin_requirement, weighted_pnl, worst_case_liability_value, base_asset_value) = + calculate_perp_position_value_and_pnl( + market_position, + market, + oracle_price_data, + &strict_quote_price, + context.margin_type, + user_custom_margin_ratio, + user_high_leverage_mode, + )?; calculation.update_fuel_perp_bonus( market, @@ -556,7 +552,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( "e_spot_market, &SpotBalanceType::Deposit, )?; - + let quote_token_value = get_strict_token_value( quote_token_amount.cast::()?, quote_spot_market.decimals, @@ -579,7 +575,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_liability_value, MarketIdentifier::perp(market.market_index), )?; - + calculation.add_total_collateral(weighted_pnl)?; } @@ -948,9 +944,14 @@ pub fn calculate_user_equity( is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; if market_position.is_isolated() { - let quote_token_amount = market_position.get_isolated_position_token_amount("e_spot_market)?; + let quote_token_amount = + market_position.get_isolated_position_token_amount("e_spot_market)?; - let token_value = get_token_value(quote_token_amount.cast()?, quote_spot_market.decimals, quote_oracle_price_data.price)?; + let token_value = get_token_value( + quote_token_amount.cast()?, + quote_spot_market.decimals, + quote_oracle_price_data.price, + )?; net_usd_value = net_usd_value.safe_add(token_value)?; } diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 544480be2e..85e2928959 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -860,9 +860,13 @@ pub fn calculate_max_perp_order_size( let is_isolated_position = user.perp_positions[position_index].is_isolated(); let free_collateral_before = if is_isolated_position { - margin_calculation.get_isolated_position_free_collateral(market_index)?.cast::()? + margin_calculation + .get_isolated_position_free_collateral(market_index)? + .cast::()? } else { - margin_calculation.get_cross_margin_free_collateral()?.cast::()? + margin_calculation + .get_cross_margin_free_collateral()? + .cast::()? }; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index 8f008c0f35..998970480d 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -199,8 +199,5 @@ pub fn get_new_position_amounts( .base_asset_amount .safe_add(delta.base_asset_amount)?; - Ok(( - new_base_asset_amount, - new_quote_asset_amount, - )) + Ok((new_base_asset_amount, new_quote_asset_amount)) } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index bb3c2c54d2..662db067ee 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -1,13 +1,37 @@ use solana_program::msg; -use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap}, state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; - -use super::{events::LiquidationBitFlag, perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; +use crate::{ + controller::{ + spot_balance::update_spot_balances, + spot_position::update_spot_balances_and_cumulative_deposits, + }, + error::{DriftResult, ErrorCode}, + math::{ + bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, + liquidation::calculate_max_pct_to_liquidate, + margin::calculate_user_safest_position_tiers, + safe_unwrap::SafeUnwrap, + }, + state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, + validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX, +}; + +use super::{ + events::LiquidationBitFlag, + perp_market::ContractTier, + perp_market_map::PerpMarketMap, + spot_market::{AssetTier, SpotBalanceType, SpotMarket}, + spot_market_map::SpotMarketMap, + user::{MarketType, User}, +}; pub trait LiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult; - fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult; + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult; fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult; @@ -34,13 +58,21 @@ pub trait LiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; - fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)>; + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)>; fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()>; fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult; - fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)>; + fn calculate_user_safest_position_tiers( + &self, + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + ) -> DriftResult<(AssetTier, ContractTier)>; fn decrease_spot_token_amount( &self, @@ -53,8 +85,18 @@ pub trait LiquidatePerpMode { fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult; } -pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { - Box::new(CrossMarginLiquidatePerpMode::new(market_index)) +pub fn get_perp_liquidation_mode( + user: &User, + market_index: u16, +) -> DriftResult> { + let perp_position = user.get_perp_position(market_index)?; + let mode: Box = if perp_position.is_isolated() { + Box::new(IsolatedLiquidatePerpMode::new(market_index)) + } else { + Box::new(CrossMarginLiquidatePerpMode::new(market_index)) + }; + + Ok(mode) } pub struct CrossMarginLiquidatePerpMode { @@ -72,7 +114,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.is_cross_margin_being_liquidated()) } - fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult { Ok(margin_calculation.cross_margin_meets_margin_requirement()) } @@ -125,8 +170,15 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_cross_margin_bankruptcy()) } - fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { - Ok((margin_calculation.margin_requirement, margin_calculation.total_collateral, 0)) + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)> { + Ok(( + margin_calculation.margin_requirement, + margin_calculation.total_collateral, + 0, + )) } fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { @@ -163,7 +215,12 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(token_amount) } - fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)> { + fn calculate_user_safest_position_tiers( + &self, + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + ) -> DriftResult<(AssetTier, ContractTier)> { calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map) } @@ -208,7 +265,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.is_isolated_position_being_liquidated(self.market_index) } - fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult { margin_calculation.isolated_position_meets_margin_requirement(self.market_index) } @@ -255,9 +315,19 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_bankruptcy(self.market_index) } - fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { - let isolated_position_margin_calculation = margin_calculation.isolated_position_margin_calculation.get(&self.market_index).safe_unwrap()?; - Ok((isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral, LiquidationBitFlag::IsolatedPosition as u8)) + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)> { + let isolated_position_margin_calculation = margin_calculation + .isolated_position_margin_calculation + .get(&self.market_index) + .safe_unwrap()?; + Ok(( + isolated_position_margin_calculation.margin_requirement, + isolated_position_margin_calculation.total_collateral, + LiquidationBitFlag::IsolatedPosition as u8, + )) } fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { @@ -270,8 +340,9 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn get_spot_token_amount(&self, user: &User, spot_market: &SpotMarket) -> DriftResult { let isolated_perp_position = user.get_isolated_perp_position(self.market_index)?; - - let token_amount = isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + + let token_amount = + isolated_perp_position.get_isolated_position_token_amount(spot_market)?; validate!( token_amount != 0, @@ -283,7 +354,12 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { Ok(token_amount) } - fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)> { + fn calculate_user_safest_position_tiers( + &self, + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + ) -> DriftResult<(AssetTier, ContractTier)> { let contract_tier = perp_market_map.get_ref(&self.market_index)?.contract_tier; Ok((AssetTier::default(), contract_tier)) @@ -312,4 +388,4 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { margin_calculation.isolated_position_margin_shortage(self.market_index) } -} \ No newline at end of file +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4db918b828..b55e11fed3 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -198,9 +198,9 @@ pub struct IsolatedPositionMarginCalculation { } impl IsolatedPositionMarginCalculation { - pub fn get_total_collateral_plus_buffer(&self) -> i128 { - self.total_collateral.saturating_add(self.total_collateral_buffer) + self.total_collateral + .saturating_add(self.total_collateral_buffer) } pub fn meets_margin_requirement(&self) -> bool { @@ -212,7 +212,11 @@ impl IsolatedPositionMarginCalculation { } pub fn margin_shortage(&self) -> DriftResult { - Ok(self.margin_requirement_plus_buffer.cast::()?.safe_sub(self.get_total_collateral_plus_buffer())?.unsigned_abs()) + Ok(self + .margin_requirement_plus_buffer + .cast::()? + .safe_sub(self.get_total_collateral_plus_buffer())? + .unsigned_abs()) } } @@ -283,9 +287,16 @@ impl MarginCalculation { Ok(()) } - pub fn add_isolated_position_margin_calculation(&mut self, market_index: u16, deposit_value: i128, pnl: i128, liability_value: u128, margin_requirement: u128) -> DriftResult { + pub fn add_isolated_position_margin_calculation( + &mut self, + market_index: u16, + deposit_value: i128, + pnl: i128, + liability_value: u128, + margin_requirement: u128, + ) -> DriftResult { let total_collateral = deposit_value.safe_add(pnl)?; - + let total_collateral_buffer = if self.context.margin_buffer > 0 && pnl < 0 { pnl.safe_mul(self.context.margin_buffer.cast::()?)? / MARGIN_PRECISION_I128 } else { @@ -293,7 +304,9 @@ impl MarginCalculation { }; let margin_requirement_plus_buffer = if self.context.margin_buffer > 0 { - margin_requirement.safe_add(liability_value.safe_mul(self.context.margin_buffer)? / MARGIN_PRECISION_U128)? + margin_requirement.safe_add( + liability_value.safe_mul(self.context.margin_buffer)? / MARGIN_PRECISION_U128, + )? } else { 0 }; @@ -305,13 +318,14 @@ impl MarginCalculation { margin_requirement_plus_buffer, }; - self.isolated_position_margin_calculation.insert(market_index, isolated_position_margin_calculation); + self.isolated_position_margin_calculation + .insert(market_index, isolated_position_margin_calculation); if let Some(market_to_track) = self.market_to_track_margin_requirement() { if market_to_track == MarketIdentifier::perp(market_index) { self.tracked_market_margin_requirement = self .tracked_market_margin_requirement - .safe_add(margin_requirement_plus_buffer)?; + .safe_add(margin_requirement)?; } } @@ -402,7 +416,8 @@ impl MarginCalculation { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + { if !isolated_position_margin_calculation.meets_margin_requirement() { return false; } @@ -412,18 +427,20 @@ impl MarginCalculation { } pub fn meets_margin_requirement_with_buffer(&self) -> bool { - let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement_with_buffer(); + let cross_margin_meets_margin_requirement = + self.cross_margin_meets_margin_requirement_with_buffer(); if !cross_margin_meets_margin_requirement { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { return false; } } - + true } @@ -438,13 +455,27 @@ impl MarginCalculation { } #[inline(always)] - pub fn isolated_position_meets_margin_requirement(&self, market_index: u16) -> DriftResult { - Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement()) + pub fn isolated_position_meets_margin_requirement( + &self, + market_index: u16, + ) -> DriftResult { + Ok(self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement()) } #[inline(always)] - pub fn isolated_position_meets_margin_requirement_with_buffer(&self, market_index: u16) -> DriftResult { - Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) + pub fn isolated_position_meets_margin_requirement_with_buffer( + &self, + market_index: u16, + ) -> DriftResult { + Ok(self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement_with_buffer()) } pub fn cross_margin_can_exit_liquidation(&self) -> DriftResult { @@ -461,8 +492,12 @@ impl MarginCalculation { msg!("liquidation mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - - Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) + + Ok(self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement_with_buffer()) } pub fn cross_margin_margin_shortage(&self) -> DriftResult { @@ -484,7 +519,10 @@ impl MarginCalculation { return Err(ErrorCode::InvalidMarginCalculation); } - self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.margin_shortage() + self.isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .margin_shortage() } pub fn tracked_market_margin_shortage(&self, margin_shortage: u128) -> DriftResult { @@ -504,9 +542,7 @@ impl MarginCalculation { Some(isolated_position_margin_calculation) => { isolated_position_margin_calculation.margin_requirement } - None => { - self.margin_requirement - } + None => self.margin_requirement, } } else { self.margin_requirement @@ -529,9 +565,17 @@ impl MarginCalculation { } pub fn get_isolated_position_free_collateral(&self, market_index: u16) -> DriftResult { - let isolated_position_margin_calculation = self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?; - isolated_position_margin_calculation.total_collateral - .safe_sub(isolated_position_margin_calculation.margin_requirement.cast::()?)? + let isolated_position_margin_calculation = self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()?; + isolated_position_margin_calculation + .total_collateral + .safe_sub( + isolated_position_margin_calculation + .margin_requirement + .cast::()?, + )? .max(0) .cast() } @@ -639,8 +683,13 @@ impl MarginCalculation { Ok(()) } - pub fn get_isolated_position_margin_calculation(&self, market_index: u16) -> DriftResult<&IsolatedPositionMarginCalculation> { - if let Some(isolated_position_margin_calculation) = self.isolated_position_margin_calculation.get(&market_index) { + pub fn get_isolated_position_margin_calculation( + &self, + market_index: u16, + ) -> DriftResult<&IsolatedPositionMarginCalculation> { + if let Some(isolated_position_margin_calculation) = + self.isolated_position_margin_calculation.get(&market_index) + { Ok(isolated_position_margin_calculation) } else { Err(ErrorCode::InvalidMarginCalculation) @@ -648,6 +697,7 @@ impl MarginCalculation { } pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { - self.isolated_position_margin_calculation.contains_key(&market_index) + self.isolated_position_margin_calculation + .contains_key(&market_index) } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 96ecd127bf..6fbef39b6e 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -382,7 +382,6 @@ impl User { self.liquidation_margin_freed = 0; self.last_active_slot = slot; - let liquidation_id = if self.has_isolated_position_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { @@ -410,15 +409,22 @@ impl User { } pub fn has_isolated_position_being_liquidated(&self) -> bool { - self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) + self.perp_positions.iter().any(|position| { + position.is_isolated() && position.is_isolated_position_being_liquidated() + }) } - pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + pub fn enter_isolated_position_liquidation( + &mut self, + perp_market_index: u16, + ) -> DriftResult { if self.is_isolated_position_being_liquidated(perp_market_index)? { return self.next_liquidation_id.safe_sub(1); } - let liquidation_id = if self.is_cross_margin_being_liquidated() || self.has_isolated_position_being_liquidated() { + let liquidation_id = if self.is_cross_margin_being_liquidated() + || self.has_isolated_position_being_liquidated() + { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -427,7 +433,7 @@ impl User { let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; - + Ok(liquidation_id) } @@ -437,7 +443,10 @@ impl User { Ok(()) } - pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { + pub fn is_isolated_position_being_liquidated( + &self, + perp_market_index: u16, + ) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; Ok(perp_position.is_isolated_position_being_liquidated()) } @@ -715,8 +724,7 @@ impl User { isolated_perp_position_market_index: u16, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; - let context = MarginContext::standard(margin_requirement_type) - .strict(strict); + let context = MarginContext::standard(margin_requirement_type).strict(strict); let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, @@ -726,7 +734,8 @@ impl User { context, )?; - let isolated_position_margin_calculation = calculation.get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; + let isolated_position_margin_calculation = calculation + .get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; validate!( calculation.all_liability_oracles_valid, @@ -1255,15 +1264,24 @@ impl PerpPosition { } pub fn is_isolated(&self) -> bool { - self.position_flag & PositionFlag::IsolatedPosition as u8 == PositionFlag::IsolatedPosition as u8 + self.position_flag & PositionFlag::IsolatedPosition as u8 + == PositionFlag::IsolatedPosition as u8 } - pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { - get_token_amount(self.isolated_position_scaled_balance as u128, spot_market, &SpotBalanceType::Deposit) + pub fn get_isolated_position_token_amount( + &self, + spot_market: &SpotMarket, + ) -> DriftResult { + get_token_amount( + self.isolated_position_scaled_balance as u128, + spot_market, + &SpotBalanceType::Deposit, + ) } pub fn is_isolated_position_being_liquidated(&self) -> bool { - self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) + != 0 } } @@ -1281,14 +1299,16 @@ impl SpotBalance for PerpPosition { } fn increase_balance(&mut self, delta: u128) -> DriftResult { - self.isolated_position_scaled_balance = - self.isolated_position_scaled_balance.safe_add(delta.cast::()?)?; + self.isolated_position_scaled_balance = self + .isolated_position_scaled_balance + .safe_add(delta.cast::()?)?; Ok(()) } fn decrease_balance(&mut self, delta: u128) -> DriftResult { - self.isolated_position_scaled_balance = - self.isolated_position_scaled_balance.safe_sub(delta.cast::()?)?; + self.isolated_position_scaled_balance = self + .isolated_position_scaled_balance + .safe_sub(delta.cast::()?)?; Ok(()) } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 09484892ab..64573306a0 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2351,4 +2351,4 @@ mod next_liquidation_id { let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); } -} \ No newline at end of file +} From 2ab06e327893aa8abb30f515a484a82d28e192a8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 17:33:09 -0400 Subject: [PATCH 29/31] program: add validate for liq borrow for perp pnl --- programs/drift/src/controller/liquidation.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 07d2ee1203..970338b0b6 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -2442,6 +2442,12 @@ pub fn liquidate_borrow_for_perp_pnl( "Perp position must have position pnl" )?; + validate!( + !user_position.is_isolated_position(), + ErrorCode::InvalidPerpPositionToLiquidate, + "Perp position is an isolated position" + )?; + let market = perp_market_map.get_ref(&perp_market_index)?; let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; From 9a5632650d56837f71207b76852ada3bde81769c Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 18:17:40 -0400 Subject: [PATCH 30/31] program: add test for isolated margin calc --- programs/drift/src/controller/liquidation.rs | 2 +- programs/drift/src/math/margin/tests.rs | 177 +++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 970338b0b6..d02307ce86 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -2443,7 +2443,7 @@ pub fn liquidate_borrow_for_perp_pnl( )?; validate!( - !user_position.is_isolated_position(), + !user_position.is_isolated(), ErrorCode::InvalidPerpPositionToLiquidate, "Perp position is an isolated position" )?; diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 2ad60a5b9e..5a858c3123 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4312,3 +4312,180 @@ mod pools { assert_eq!(result.unwrap_err(), ErrorCode::InvalidPoolId) } } + +#[cfg(test)] +mod isolated_position { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, + PEG_PRECISION, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::state::margin_calculation::{MarginCalculation, MarginContext}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{MarketStatus, 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::user::{Order, PerpPosition, PositionFlag, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::{get_positions, get_pyth_price}; + use crate::{create_account_info}; + + #[test] + pub fn isolated_position_margin_requirement() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_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, + order_step_size: 10000000, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..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 usdc_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, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 20000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 100 * BASE_PRECISION_I64, + quote_asset_amount: -11000 * QUOTE_PRECISION_I64, + position_flag: PositionFlag::IsolatedPosition as u8, + isolated_position_scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + let cross_margin_margin_requirement = margin_calculation.margin_requirement; + let cross_total_collateral = margin_calculation.total_collateral; + + let isolated_margin_calculation = margin_calculation.get_isolated_position_margin_calculation(0).unwrap(); + let isolated_margin_requirement = isolated_margin_calculation.margin_requirement; + let isolated_total_collateral = isolated_margin_calculation.total_collateral; + + assert_eq!(cross_margin_margin_requirement, 12000000000); + assert_eq!(cross_total_collateral, 20000000000); + assert_eq!(isolated_margin_requirement, 1000000000); + assert_eq!(isolated_total_collateral, -900000000); + assert_eq!(margin_calculation.meets_margin_requirement(), false); + assert_eq!(margin_calculation.cross_margin_meets_margin_requirement(), true); + assert_eq!(isolated_margin_calculation.meets_margin_requirement(), false); + assert_eq!(margin_calculation.isolated_position_meets_margin_requirement(0).unwrap(), false); + + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).margin_buffer(1000), + ) + .unwrap(); + + let cross_margin_margin_requirement = margin_calculation.margin_requirement_plus_buffer; + let cross_total_collateral = margin_calculation.get_total_collateral_plus_buffer(); + + let isolated_margin_calculation = margin_calculation.get_isolated_position_margin_calculation(0).unwrap(); + let isolated_margin_requirement = isolated_margin_calculation.margin_requirement_plus_buffer; + let isolated_total_collateral = isolated_margin_calculation.get_total_collateral_plus_buffer(); + + assert_eq!(cross_margin_margin_requirement, 13000000000); + assert_eq!(cross_total_collateral, 20000000000); + assert_eq!(isolated_margin_requirement, 2000000000); + assert_eq!(isolated_total_collateral, -1000000000); + } +} \ No newline at end of file From b171c23e3be180be2671e86022026e5cd434cb2d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 18:35:48 -0400 Subject: [PATCH 31/31] is bankrupt test --- programs/drift/src/math/bankruptcy/tests.rs | 43 ++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index 01ba6aad7c..371ab80461 100644 --- a/programs/drift/src/math/bankruptcy/tests.rs +++ b/programs/drift/src/math/bankruptcy/tests.rs @@ -1,6 +1,6 @@ use crate::math::bankruptcy::is_user_bankrupt; use crate::state::spot_market::SpotBalanceType; -use crate::state::user::{PerpPosition, SpotPosition, User}; +use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User}; use crate::test_utils::{get_positions, get_spot_positions}; #[test] @@ -81,3 +81,44 @@ fn user_with_empty_position_and_balances() { let is_bankrupt = is_user_bankrupt(&user); assert!(!is_bankrupt); } + +#[test] +fn user_with_isolated_position() { + let user = User { + perp_positions: get_positions(PerpPosition { + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut user_with_scaled_balance = user.clone(); + user_with_scaled_balance.perp_positions[0].isolated_position_scaled_balance = 1000000000000000000; + + let is_bankrupt = is_user_bankrupt(&user_with_scaled_balance); + assert!(!is_bankrupt); + + let mut user_with_base_asset_amount = user.clone(); + user_with_base_asset_amount.perp_positions[0].base_asset_amount = 1000000000000000000; + + let is_bankrupt = is_user_bankrupt(&user_with_base_asset_amount); + assert!(!is_bankrupt); + + let mut user_with_open_order = user.clone(); + user_with_open_order.perp_positions[0].open_orders = 1; + + let is_bankrupt = is_user_bankrupt(&user_with_open_order); + assert!(!is_bankrupt); + + let mut user_with_positive_pnl = user.clone(); + user_with_positive_pnl.perp_positions[0].quote_asset_amount = 1000000000000000000; + + let is_bankrupt = is_user_bankrupt(&user_with_positive_pnl); + assert!(!is_bankrupt); + + let mut user_with_negative_pnl = user.clone(); + user_with_negative_pnl.perp_positions[0].quote_asset_amount = -1000000000000000000; + + let is_bankrupt = is_user_bankrupt(&user_with_negative_pnl); + assert!(is_bankrupt); +}